feat: Support pushing certificates to other nodes (#10074)

Refs https://github.com/1Panel-dev/1Panel/issues/9103
This commit is contained in:
CityFun 2025-08-20 16:41:19 +08:00 committed by GitHub
parent d6f16cf700
commit d09d686378
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 288 additions and 67 deletions

View file

@ -1,6 +1,7 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"net/http"
"net/url"
"reflect"
@ -245,3 +246,15 @@ func (b *BaseApi) DownloadWebsiteSSL(c *gin.Context) {
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
}
func (b *BaseApi) ImportMasterSSL(c *gin.Context) {
var req model.WebsiteSSL
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteSSLService.ImportMasterSSL(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}

View file

@ -9,24 +9,26 @@ type WebsiteSSLSearch struct {
}
type WebsiteSSLCreate struct {
PrimaryDomain string `json:"primaryDomain" validate:"required"`
OtherDomains string `json:"otherDomains"`
Provider string `json:"provider" validate:"required"`
AcmeAccountID uint `json:"acmeAccountId" validate:"required"`
DnsAccountID uint `json:"dnsAccountId"`
AutoRenew bool `json:"autoRenew"`
KeyType string `json:"keyType"`
Apply bool `json:"apply"`
PushDir bool `json:"pushDir"`
Dir string `json:"dir"`
ID uint `json:"id"`
Description string `json:"description"`
DisableCNAME bool `json:"disableCNAME"`
SkipDNS bool `json:"skipDNS"`
Nameserver1 string `json:"nameserver1"`
Nameserver2 string `json:"nameserver2"`
ExecShell bool `json:"execShell"`
Shell string `json:"shell"`
PrimaryDomain string `json:"primaryDomain" validate:"required"`
OtherDomains string `json:"otherDomains"`
Provider string `json:"provider" validate:"required"`
AcmeAccountID uint `json:"acmeAccountId" validate:"required"`
DnsAccountID uint `json:"dnsAccountId"`
AutoRenew bool `json:"autoRenew"`
KeyType string `json:"keyType"`
Apply bool `json:"apply"`
PushDir bool `json:"pushDir"`
Dir string `json:"dir"`
ID uint `json:"id"`
Description string `json:"description"`
DisableCNAME bool `json:"disableCNAME"`
SkipDNS bool `json:"skipDNS"`
Nameserver1 string `json:"nameserver1"`
Nameserver2 string `json:"nameserver2"`
ExecShell bool `json:"execShell"`
Shell string `json:"shell"`
PushNode bool `json:"pushNode"`
Nodes []string `json:"nodes"`
}
type WebsiteDNSReq struct {

View file

@ -36,6 +36,9 @@ type WebsiteSSL struct {
DisableCNAME bool `json:"disableCNAME"`
ExecShell bool `json:"execShell"`
Shell string `json:"shell"`
MasterSSLID uint `json:"masterSslId"`
Nodes string `json:"nodes"`
PushNode bool `json:"pushNode"`
AcmeAccount WebsiteAcmeAccount `json:"acmeAccount" gorm:"-:migration"`
DnsAccount WebsiteDnsAccount `json:"dnsAccount" gorm:"-:migration"`

View file

@ -17,6 +17,7 @@ type ISSLRepo interface {
WithByDnsAccountId(dnsAccountId uint) DBOption
WithByCAID(caID uint) DBOption
WithByDomain(domain string) DBOption
WithByMasterSSLID(sslID uint) DBOption
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteSSL, error)
GetFirst(opts ...DBOption) (*model.WebsiteSSL, error)
List(opts ...DBOption) ([]model.WebsiteSSL, error)
@ -52,12 +53,19 @@ func (w WebsiteSSLRepo) WithByCAID(caID uint) DBOption {
return db.Where("ca_id = ?", caID)
}
}
func (w WebsiteSSLRepo) WithByDomain(domain string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("primary_domain Like ? or domains Like ?", "%"+domain+"%", "%"+domain+"%")
}
}
func (w WebsiteSSLRepo) WithByMasterSSLID(sslID uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("master_ssl_id = ?", sslID)
}
}
func (w WebsiteSSLRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteSSL, error) {
var sslList []model.WebsiteSSL
db := getDb(opts...).Model(&model.WebsiteSSL{})

View file

@ -5,6 +5,7 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/go-acme/lego/v4/certificate"
"log"
"os"
@ -46,6 +47,7 @@ type IWebsiteSSLService interface {
ObtainSSL(apply request.WebsiteSSLApply) error
SyncForRestart() error
DownloadFile(id uint) (*os.File, error)
ImportMasterSSL(create model.WebsiteSSL) error
}
func NewIWebsiteSSLService() IWebsiteSSLService {
@ -146,6 +148,10 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
}
websiteSSL.Dir = create.Dir
}
if create.PushNode && global.IsMaster && len(create.Nodes) > 0 {
websiteSSL.PushNode = true
websiteSSL.Nodes = strings.Join(create.Nodes, ",")
}
var domains []string
if create.OtherDomains != "" {
@ -207,7 +213,7 @@ func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{
}
func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
if global.CoreDB == nil {
if !global.IsMaster {
return
}
systemSSLEnable, sslID := GetSystemSSL()
@ -387,6 +393,14 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
printSSLLog(logger, "ApplyWebSiteSSLSuccess", nil, apply.DisableLog)
}
reloadSystemSSL(websiteSSL, logger)
if websiteSSL.PushNode {
printSSLLog(logger, "StartPushSSLToNode", nil, apply.DisableLog)
if err = xpack.PushSSLToNode(websiteSSL); err != nil {
printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()}, apply.DisableLog)
return
}
printSSLLog(logger, "PushSSLToNodeSuccess", nil, apply.DisableLog)
}
}()
return nil
@ -712,3 +726,55 @@ func (w WebsiteSSLService) SyncForRestart() error {
}
return nil
}
func (w WebsiteSSLService) ImportMasterSSL(create model.WebsiteSSL) error {
websiteSSL, _ := websiteSSLRepo.GetFirst(websiteSSLRepo.WithByMasterSSLID(create.ID))
if websiteSSL == nil {
websiteSSL = &model.WebsiteSSL{
Status: constant.SSLReady,
Provider: constant.FromMaster,
PrimaryDomain: create.PrimaryDomain,
StartDate: create.StartDate,
ExpireDate: create.ExpireDate,
KeyType: create.KeyType,
Description: create.Description,
MasterSSLID: create.ID,
PrivateKey: create.PrivateKey,
Pem: create.Pem,
Type: create.Type,
Organization: create.Organization,
}
if err := websiteSSLRepo.Create(context.TODO(), websiteSSL); err != nil {
return err
}
} else {
websiteSSL.PrimaryDomain = create.PrimaryDomain
websiteSSL.StartDate = create.StartDate
websiteSSL.ExpireDate = create.ExpireDate
websiteSSL.KeyType = create.KeyType
websiteSSL.Description = create.Description
websiteSSL.PrivateKey = create.PrivateKey
websiteSSL.Pem = create.Pem
websiteSSL.Type = create.Type
websiteSSL.Organization = create.Organization
if err := websiteSSLRepo.Save(websiteSSL); err != nil {
return err
}
}
websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(websiteSSL.ID))
if len(websites) == 0 {
return nil
}
for _, website := range websites {
if err := createPemFile(website, *websiteSSL); err != nil {
continue
}
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err == nil {
if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
return err
}
}
return nil
}

View file

@ -25,6 +25,7 @@ const (
Http = "http"
Manual = "manual"
SelfSigned = "selfSigned"
FromMaster = "fromMaster"
StartWeb = "start"
StopWeb = "stop"

View file

@ -162,6 +162,9 @@ StartUpdateSystemSSL: 'Start updating system certificate'
UpdateSystemSSLSuccess: 'Update system certificate successfully'
ErrWildcardDomain: 'Unable to apply for wildcard domain name certificate in HTTP mode'
ErrApplySSLCanNotDelete: "The certificate {{.name}} being applied for cannot be deleted, please try again later."
StartPushSSLToNode: "Starting to push certificate to node"
PushSSLToNodeFailed: "Failed to push certificate to node: {{ .err }}"
PushSSLToNodeSuccess: "Successfully pushed certificate to node"
#mysql
ErrUserIsExist: 'The current user already exists, please re-enter'

View file

@ -162,6 +162,9 @@ StartUpdateSystemSSL: 'システム証明書の更新を開始します'
UpdateSystemSSLSuccess: 'システム証明書を正常に更新しました'
ErrWildcardDomain: 'HTTP モードでワイルドカード ドメイン名証明書を申請できません'
ErrApplySSLCanNotDelete: "申請中の証明書 {{.name}} は削除できません。しばらくしてからもう一度お試しください。"
StartPushSSLToNode: "証明書をノードにプッシュ開始"
PushSSLToNodeFailed: "ノードに証明書をプッシュ失敗: {{ .err }}"
PushSSLToNodeSuccess: "ノードに証明書をプッシュ成功"
#mysql
ErrUserIsExist: '現在のユーザーは既に存在します。再入力してください'

View file

@ -162,6 +162,9 @@ StartUpdateSystemSSL: '시스템 인증서 업데이트 시작'
UpdateSystemSSLSuccess: '시스템 인증서 업데이트가 성공적으로 완료되었습니다.'
ErrWildcardDomain: 'HTTP 모드에서 와일드카드 도메인 이름 인증서를 신청할 수 없습니다'
ErrApplySSLCanNotDelete: "신청 중인 인증서 {{.name}}는 삭제할 수 없습니다. 나중에 다시 시도해 주세요."
StartPushSSLToNode: "인증서를 노드로 푸시 시작"
PushSSLToNodeFailed: "노드로 인증서 푸시 실패: {{ .err }}"
PushSSLToNodeSuccess: "노드로 인증서 푸시 성공"
#마이SQL
ErrUserIsExist: '현재 사용자가 이미 존재합니다. 다시 입력하세요'

View file

@ -14,6 +14,9 @@ ErrApiConfigKeyInvalid: 'Ralat kunci antara muka API: {{ .detail }}'
ErrApiConfigIPInvalid: 'IP yang digunakan untuk memanggil antara muka API tiada dalam senarai putih: {{ .detail }}'
ErrApiConfigDisable: 'Antara muka ini melarang penggunaan panggilan antara muka API: {{ .detail }}'
ErrApiConfigKeyTimeInvalid: 'Ralat cap masa antara muka API: {{ .detail }}'
StartPushSSLToNode: "Mula menolak sijil ke nod"
PushSSLToNodeFailed: "Gagal menolak sijil ke nod: {{ .err }}"
PushSSLToNodeSuccess: "Berjaya menolak sijil ke nod"
#biasa
ErrUsernameIsExist: 'Nama pengguna sudah wujud'

View file

@ -14,6 +14,9 @@ ErrApiConfigKeyInvalid: 'Erro de chave da interface da API: {{ .detail }}'
ErrApiConfigIPInvalid: 'O IP usado para chamar a interface da API não está na lista de permissões: {{ .detail }}'
ErrApiConfigDisable: 'Esta interface proíbe o uso de chamadas de interface de API: {{ .detail }}'
ErrApiConfigKeyTimeInvalid: 'Erro de registro de data e hora da interface da API: {{ .detail }}'
StartPushSSLToNode: "Iniciando o envio do certificado para o nó"
PushSSLToNodeFailed: "Falha ao enviar o certificado para o nó: {{ .err }}"
PushSSLToNodeSuccess: "Certificado enviado com sucesso para o nó"
#comum
ErrUsernameIsExist: 'Nome de usuário já existe'

View file

@ -14,6 +14,9 @@ ErrApiConfigKeyInvalid: 'Ошибка ключа интерфейса API: {{ .d
ErrApiConfigIPInvalid: 'IP-адрес, используемый для вызова интерфейса API, отсутствует в белом списке: {{ .detail }}'
ErrApiConfigDisable: 'Этот интерфейс запрещает использование вызовов интерфейса API: {{ .detail }}'
ErrApiConfigKeyTimeInvalid: 'Ошибка временной метки интерфейса API: {{ .detail }}'
StartPushSSLToNode: "Начало отправки сертификата на узел"
PushSSLToNodeFailed: "Не удалось отправить сертификат на узел: {{ .err }}"
PushSSLToNodeSuccess: "Сертификат успешно отправлен на узел"
#общий
ErrUsernameIsExist: 'Имя пользователя уже существует'

View file

@ -14,6 +14,9 @@ ErrApiConfigKeyInvalid: 'API arayüz anahtarı hatası: {{ .detail }}'
ErrApiConfigIPInvalid: 'API arayüzünü çağırmak için kullanılan IP beyaz listede değil: {{ .detail }}'
ErrApiConfigDisable: 'Bu arayüz API arayüz çağrılarının kullanımını yasaklıyor: {{ .detail }}'
ErrApiConfigKeyTimeInvalid: 'API arayüz zaman damgası hatası: {{ .detail }}'
StartPushSSLToNode: "Sertifika düğüme gönderilmeye başlandı"
PushSSLToNodeFailed: "Sertifika düğüme gönderilemedi: {{ .err }}"
PushSSLToNodeSuccess: "Sertifika düğüme başarıyla gönderildi"
#common
ErrUsernameIsExist: 'Kullanıcı adı zaten mevcut'

View file

@ -161,6 +161,9 @@ StartUpdateSystemSSL: '開始更新系統憑證'
UpdateSystemSSLSuccess: '更新系統憑證成功'
ErrWildcardDomain: 'HTTP 模式無法申請泛網域憑證'
ErrApplySSLCanNotDelete: "正在申請的證書 {{.name}} 無法刪除,請稍後再試"
StartPushSSLToNode: "開始推送證書到節點"
PushSSLToNodeFailed: "推送證書到節點失敗: {{ .err }}"
PushSSLToNodeSuccess: "推送證書到節點成功"
#mysql
ErrUserIsExist: '目前使用者已存在,請重新輸入'

View file

@ -161,6 +161,9 @@ StartUpdateSystemSSL: "开始更新系统证书"
UpdateSystemSSLSuccess: "更新系统证书成功"
ErrWildcardDomain: "HTTP 模式无法申请泛域名证书"
ErrApplySSLCanNotDelete: "正在申请的证书{{.name}}无法删除,请稍后再试"
StartPushSSLToNode: "开始推送证书到节点"
PushSSLToNodeFailed: "推送证书到节点失败: {{ .err }}"
PushSSLToNodeSuccess: "推送证书到节点成功"
#mysql
ErrUserIsExist: "当前用户已存在,请重新输入"

View file

@ -36,6 +36,7 @@ func InitAgentDB() {
migrations.UpdateMcpServer,
migrations.InitCronjobGroup,
migrations.AddColumnToAlert,
migrations.UpdateWebsiteSSL,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -461,3 +461,13 @@ var AddColumnToAlert = &gormigrate.Migration{
return nil
},
}
var UpdateWebsiteSSL = &gormigrate.Migration{
ID: "20250819-update-website-ssl",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.WebsiteSSL{}); err != nil {
return err
}
return nil
},
}

View file

@ -23,5 +23,6 @@ func (a *WebsiteSSLRouter) InitRouter(Router *gin.RouterGroup) {
groupRouter.POST("/upload", baseApi.UploadWebsiteSSL)
groupRouter.POST("/obtain", baseApi.ApplyWebsiteSSL)
groupRouter.POST("/download", baseApi.DownloadWebsiteSSL)
groupRouter.POST("/import", baseApi.ImportMasterSSL)
}
}

View file

@ -181,7 +181,8 @@ func getCaDirURL(accountType, customCaURL string) string {
var caDirURL string
switch accountType {
case "letsencrypt":
caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
//caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
caDirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
case "zerossl":
caDirURL = "https://acme.zerossl.com/v2/DV90"
case "buypass":

View file

@ -85,3 +85,7 @@ func LoadRequestTransport() *http.Transport {
func ValidateCertificate(c *gin.Context) bool {
return true
}
func PushSSLToNode(websiteSSL *model.WebsiteSSL) error {
return nil
}

View file

@ -227,4 +227,7 @@ ErrMasterDelete: "Unable to delete the master node, please delete the slave node
ClusterNameIsExist: "Cluster name already exists."
AppStatusUnHealthy: "Application status acquisition is abnormal, please check the installation node status in the node list."
MasterNodePortNotAvailable: "Node {{ .name }} port {{ .port }} connectivity verification failed, please check firewall/security group settings and master node status."
ClusterMasterNotExist: "The master node of the cluster is disconnected, please delete the child nodes."
ClusterMasterNotExist: "The master node of the cluster is disconnected, please delete the child nodes."
#ssl
ErrReqFailed: "{{.name}} request failed: {{ .err }}"

View file

@ -228,4 +228,7 @@ ErrMasterDelete: "マスターノードを削除できません。スレーブ
ClusterNameIsExist: "クラスタ名は既に存在します。"
AppStatusUnHealthy: "アプリケーションのステータス取得が異常です。ノードリストでインストールノードのステータスを確認してください。"
MasterNodePortNotAvailable: "ノード {{ .name }} のポート {{ .port }} の接続性検証が失敗しました。ファイアウォール/セキュリティグループの設定とマスターノードのステータスを確認してください。"
ClusterMasterNotExist: "クラスタのマスターノードが切断されています。子ノードを削除してください。"
ClusterMasterNotExist: "クラスタのマスターノードが切断されています。子ノードを削除してください。"
#ssl
ErrReqFailed: "{{.name}} リクエスト失敗: {{ .err }}"

View file

@ -227,4 +227,7 @@ ErrMasterDelete: "마스터 노드를 삭제할 수 없습니다. 슬레이브
ClusterNameIsExist: "클러스터 이름이 이미 존재합니다."
AppStatusUnHealthy: "애플리케이션 상태 획득이 비정상입니다. 노드 목록에서 설치 노드 상태를 확인하세요."
MasterNodePortNotAvailable: "노드 {{ .name }} 포트 {{ .port }} 연결성 검증에 실패했습니다. 방화벽/보안 그룹 설정 및 마스터 노드 상태를 확인하세요."
ClusterMasterNotExist: "클러스터의 마스터 노드가 연결이 끊어졌습니다. 자식 노드를 삭제하세요."
ClusterMasterNotExist: "클러스터의 마스터 노드가 연결이 끊어졌습니다. 자식 노드를 삭제하세요."
#ssl
ErrReqFailed: "{{.name}} 요청 실패: {{ .err }}"

View file

@ -222,4 +222,7 @@ ErrMasterDelete: "Tidak dapat menghapus nod utama, sila hapuskan nod perantara d
ClusterNameIsExist: "Nama kluster sudah wujud."
AppStatusUnHealthy: "Pengambilan status aplikasi tidak normal, sila periksa status nod pemasangan dalam senarai nod."
MasterNodePortNotAvailable: "Pengesahan kesambungan pelabuhan {{ .name }} nod {{ .port }} gagal, sila periksa tetapan firewall/kumpulan keselamatan dan status nod utama."
ClusterMasterNotExist: "Node utama kluster terputus, sila padamkan nod anak."
ClusterMasterNotExist: "Node utama kluster terputus, sila padamkan nod anak."
#ssl
ErrReqFailed: "{{.name}} permintaan gagal: {{ .err }}"

View file

@ -227,4 +227,7 @@ ErrMasterDelete: "Não é possível excluir o nó mestre, exclua os nós escravo
ClusterNameIsExist: "O nome do cluster já existe."
AppStatusUnHealthy: "A aquisição do status do aplicativo está anormal, verifique o status dos nós de instalação na lista de nós."
MasterNodePortNotAvailable: "A verificação de conectividade da porta {{ .port }} do nó {{ .name }} falhou, verifique as configurações de firewall/grupo de segurança e o status do nó mestre."
ClusterMasterNotExist: "O nó mestre do cluster está desconectado, por favor, exclua os nós filhos."
ClusterMasterNotExist: "O nó mestre do cluster está desconectado, por favor, exclua os nós filhos."
#ssl
ErrReqFailed: "{{.name}} solicitação falhou: {{ .err }}

View file

@ -227,4 +227,7 @@ ErrMasterDelete: "Невозможно удалить основной узел,
ClusterNameIsExist: "Имя кластера уже существует."
AppStatusUnHealthy: "Получение статуса приложения аномально, пожалуйста, проверьте статус узлов установки в списке узлов."
MasterNodePortNotAvailable: "Проверка подключения порта {{ .port }} узла {{ .name }} не удалась, пожалуйста, проверьте настройки брандмауэра/группы безопасности и статус главного узла."
ClusterMasterNotExist: "Основной узел кластера отключен, пожалуйста, удалите дочерние узлы."
ClusterMasterNotExist: "Основной узел кластера отключен, пожалуйста, удалите дочерние узлы."
#ssl
ErrReqFailed: "{{.name}} запрос не удался: {{ .err }}"

View file

@ -226,4 +226,7 @@ ErrMasterDelete: "Ana düğümü silinemiyor, lütfen önce alt düğümleri sil
ClusterNameIsExist: "Küme adı zaten var."
AppStatusUnHealthy: "Uygulama durumu alımı anormal, lütfen düğüm listesindeki yükleme düğümü durumunu kontrol edin."
MasterNodePortNotAvailable: "Düğüm {{ .name }} portu {{ .port }} bağlantı doğrulaması başarısız oldu, lütfen güvenlik duvarı/güvenlik grubu ayarlarını ve ana düğüm durumunu kontrol edin."
ClusterMasterNotExist: "Küme ana düğümü bağlantısı kesildi, lütfen alt düğümleri silin."
ClusterMasterNotExist: "Küme ana düğümü bağlantısı kesildi, lütfen alt düğümleri silin."
#ssl
ErrReqFailed: "{{.name}} istek başarısız: {{ .err }}"

View file

@ -236,4 +236,7 @@ ErrMasterDelete: "無法刪除主節點,請先刪除從節點。"
ClusterNameIsExist: "集群名稱已存在。"
AppStatusUnHealthy: "應用獲取狀態異常,請在節點列表檢查安裝節點狀態。"
MasterNodePortNotAvailable: "節點 {{ .name }} 端口 {{ .port }} 連通性校驗失敗,請檢查防火牆/安全組設置和主節點狀態。"
ClusterMasterNotExist: "集群主節點失聯,請刪除子節點。"
ClusterMasterNotExist: "集群主節點失聯,請刪除子節點。"
#ssl
ErrReqFailed: "{{.name}} 請求失敗: {{ .err }}"

View file

@ -236,4 +236,7 @@ ErrMasterDelete: "无法删除主节点,请先删除从节点"
ClusterNameIsExist: "集群名称已存在"
AppStatusUnHealthy: "应用获取状态异常,请在节点列表检查安装节点状态"
MasterNodePortNotAvailable: "节点 {{ .name }} 端口 {{ .port }} 连通性校验失败,请检查防火墙/安全组设置和主节点状态"
ClusterMasterNotExist: "集群主节点失联,请删除子节点"
ClusterMasterNotExist: "集群主节点失联,请删除子节点"
#ssl
ErrReqFailed: "{{.name}} 请求失败: {{ .err }}"

View file

@ -44,7 +44,7 @@ func Proxy() gin.HandlerFunc {
apiReq := c.GetBool("API_AUTH")
if !apiReq && strings.HasPrefix(c.Request.URL.Path, "/api/v2/") && !checkSession(c) {
if !apiReq && strings.HasPrefix(c.Request.URL.Path, "/api/v2/") && !isLocalAPI(c.Request.URL.Path) && !checkSession(c) {
data, _ := res.ErrorMsg.ReadFile("html/401.html")
c.Data(401, "text/html; charset=utf-8", data)
c.Abort()
@ -89,3 +89,7 @@ func checkSession(c *gin.Context) bool {
_ = global.SESSION.Set(c, psession, httpsSetting.Value == constant.StatusEnable, lifeTime)
return true
}
func isLocalAPI(urlPath string) bool {
return urlPath == "/api/v2/core/xpack/sync/ssl"
}

View file

@ -146,7 +146,11 @@ const handleClose = () => {
};
const beforeClose = (done: () => void) => {
emit('beforeClose', done);
if (!props.confirmBeforeClose) {
done();
} else {
emit('beforeClose', done);
}
};
function toggleFullscreen() {

View file

@ -2669,6 +2669,9 @@ const message = {
customAcme: 'Custom ACME Service',
customAcmeURL: 'ACME Service URL',
baiduCloud: 'Baidu Cloud',
pushNode: 'Sync to Other Nodes',
pushNodeHelper: 'Push to selected nodes after application/renewal',
fromMaster: 'Master Node Push',
},
firewall: {
create: 'Create rule',

View file

@ -2582,6 +2582,9 @@ const message = {
customAcme: 'カスタム ACME サービス',
customAcmeURL: 'ACME サービス URL',
baiduCloud: '百度クラウド',
pushNode: '他のノードに同期',
pushNodeHelper: '申請/更新後に選択したノードにプッシュ',
fromMaster: 'マスターノードからのプッシュ',
},
firewall: {
create: 'ルールを作成します',

View file

@ -2535,6 +2535,9 @@ const message = {
customAcme: '사용자 정의 ACME 서비스',
customAcmeURL: 'ACME 서비스 URL',
baiduCloud: '바이두 클라우드',
pushNode: '다른 노드에 동기화',
pushNodeHelper: '신청/갱신 선택한 노드로 푸시',
fromMaster: '마스터 노드에서 푸시',
},
firewall: {
create: '규칙 만들기',

View file

@ -2642,6 +2642,9 @@ const message = {
customAcme: 'Perkhidmatan ACME Tersuai',
customAcmeURL: 'URL Perkhidmatan ACME',
baiduCloud: 'Baidu Cloud',
pushNode: 'Segerakan ke Nod Lain',
pushNodeHelper: 'Tolak ke nod terpilih selepas permohonan/pembaharuan',
fromMaster: 'Tolak dari Nod Utama',
},
firewall: {
create: 'Buat peraturan',

View file

@ -2643,6 +2643,9 @@ const message = {
customAcme: 'Serviço ACME Personalizado',
customAcmeURL: 'URL do Serviço ACME',
baiduCloud: 'Baidu Cloud',
pushNode: 'Sincronizar com Outros Nós',
pushNodeHelper: 'Enviar para os nós selecionados após a aplicação/renovação',
fromMaster: 'Envio do Mestre',
},
firewall: {
create: 'Criar regra',

View file

@ -2638,6 +2638,9 @@ const message = {
customAcme: 'Пользовательская служба ACME',
customAcmeURL: 'URL службы ACME',
baiduCloud: 'Baidu Cloud',
pushNode: 'Синхронизация с другими узлами',
pushNodeHelper: 'Отправить на выбранные узлы после заявки/продления',
fromMaster: 'Отправка с главного узла',
},
firewall: {
create: 'Создать правило',

View file

@ -2702,6 +2702,9 @@ const message = {
customAcme: 'Özel ACME Servisi',
customAcmeURL: 'ACME Servis URLsi',
baiduCloud: 'Baidu Cloud',
pushNode: 'Diğer Düğümlere Senkronize Et',
pushNodeHelper: 'Başvuru/yenilemeden sonra seçilen düğümlere gönder',
fromMaster: 'Ana Düğümden Gönder',
},
firewall: {
create: 'Kural oluştur',

View file

@ -2489,6 +2489,9 @@ const message = {
customAcme: '自訂 ACME 服務',
customAcmeURL: 'ACME 服務 URL',
baiduCloud: '百度雲',
pushNode: '同步到其他節點',
pushNodeHelper: '申請/續期之後推送到選擇的節點',
fromMaster: '主節點推送',
},
firewall: {
create: '創建規則',

View file

@ -2479,6 +2479,9 @@ const message = {
customAcme: '自定义 ACME 服务',
customAcmeURL: 'ACME 服务 URL',
baiduCloud: '百度云',
pushNode: '同步到其他节点',
pushNodeHelper: '申请/续期之后推送到选择的节点',
fromMaster: '主节点推送',
},
firewall: {
create: '创建规则',

View file

@ -440,6 +440,8 @@ export function getProvider(provider: string): string {
return 'HTTP';
case 'selfSigned':
return i18n.global.t('ssl.selfSigned');
case 'fromMaster':
return i18n.global.t('ssl.fromMaster');
default:
return i18n.global.t('ssl.manualCreate');
}

View file

@ -39,7 +39,7 @@ import { deleteOllamaModel } from '@/api/modules/ai';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { CheckboxValueType } from 'element-plus';
import { onMounted, ref } from 'vue';
import { ref } from 'vue';
defineOptions({ name: 'OpDialog' });
@ -97,8 +97,6 @@ const handleClose = () => {
open.value = false;
};
onMounted(() => {});
defineExpose({
acceptParams,
});

View file

@ -258,14 +258,6 @@ onMounted(async () => {
padding-bottom: 10px;
}
// .tag-button {
// margin-right: 10px;
// &.no-active {
// background: none;
// border: none;
// }
// }
@media only screen and (min-width: 768px) and (max-width: 1200px) {
.app-col-12 {
max-width: 50%;

View file

@ -94,28 +94,6 @@
<el-form-item :label="''" prop="autoRenew" v-if="ssl.provider !== 'dnsManual'">
<el-checkbox v-model="ssl.autoRenew" :label="$t('ssl.autoRenew')" />
</el-form-item>
<el-form-item :label="''" prop="pushDir">
<el-checkbox v-model="ssl.pushDir" :label="$t('ssl.pushDir')" />
</el-form-item>
<el-form-item :label="$t('ssl.dir')" prop="dir" v-if="ssl.pushDir">
<el-input v-model.trim="ssl.dir">
<template #prepend>
<el-button icon="Folder" @click="fileRef.acceptParams({ path: ssl.dir, dir: true })" />
</template>
</el-input>
<span class="input-help">
{{ $t('ssl.pushDirHelper') }}
</span>
</el-form-item>
<el-form-item :label="''" prop="execShell">
<el-checkbox v-model="ssl.execShell" :label="$t('ssl.execShell')" />
</el-form-item>
<el-form-item :label="$t('ssl.shell')" prop="shell" v-if="ssl.execShell">
<el-input type="textarea" :rows="4" v-model="ssl.shell" />
<span class="input-help">
{{ $t('ssl.shellHelper') }}
</span>
</el-form-item>
<div v-if="ssl.provider != 'selfSigned'">
<el-form-item :label="''" prop="disableCNAME">
<el-checkbox v-model="ssl.disableCNAME" :label="$t('ssl.disableCNAME')" />
@ -141,6 +119,35 @@
{{ $t('ssl.nameserverHelper') }}
</span>
</el-form-item>
<el-form-item :label="''" prop="pushDir">
<el-checkbox v-model="ssl.pushDir" :label="$t('ssl.pushDir')" />
</el-form-item>
<el-form-item :label="$t('ssl.dir')" prop="dir" v-if="ssl.pushDir">
<el-input v-model.trim="ssl.dir">
<template #prepend>
<el-button icon="Folder" @click="fileRef.acceptParams({ path: ssl.dir, dir: true })" />
</template>
</el-input>
<span class="input-help">
{{ $t('ssl.pushDirHelper') }}
</span>
</el-form-item>
<el-form-item :label="''" prop="execShell">
<el-checkbox v-model="ssl.execShell" :label="$t('ssl.execShell')" />
</el-form-item>
<el-form-item :label="$t('ssl.shell')" prop="shell" v-if="ssl.execShell">
<el-input type="textarea" :rows="4" v-model="ssl.shell" />
<span class="input-help">
{{ $t('ssl.shellHelper') }}
</span>
</el-form-item>
<PushtoNode
v-if="isMaster && isMasterProductPro"
:push-node="ssl.pushNode"
:nodes="ssl.nodes"
@update:push-node="ssl.pushNode = $event"
@update:nodes="ssl.nodes = $event"
/>
</div>
</el-form>
<template #footer>
@ -166,6 +173,18 @@ import { computed, reactive, ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { KeyTypes } from '@/global/mimetype';
import { getDNSName, getAccountName } from '@/utils/util';
import { defineAsyncComponent } from 'vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isMasterProductPro, isMaster } = useGlobalStore();
const PushtoNode = defineAsyncComponent(async () => {
const modules = import.meta.glob('@/xpack/views/ssl/index.vue');
const loader = modules['/src/xpack/views/ssl/index.vue'];
if (loader) {
return ((await loader()) as any).default;
}
return { template: '<div></div>' };
});
const props = defineProps({
id: {
@ -205,6 +224,7 @@ const rules = ref({
nameserver2: [Rules.ipv4],
shell: [Rules.requiredInput],
description: [checkMaxLength(128)],
nodes: [Rules.requiredSelect],
});
const websiteID = ref();
@ -227,6 +247,8 @@ const initData = () => ({
nameserver2: '',
execShell: false,
shell: '',
pushNode: false,
nodes: [],
});
const ssl = ref(initData());

View file

@ -57,7 +57,7 @@
prop="domains"
min-width="90px"
></el-table-column>
<el-table-column :label="$t('ssl.applyType')" show-overflow-tooltip prop="provider" width="90px">
<el-table-column :label="$t('ssl.applyType')" show-overflow-tooltip prop="provider" width="120px">
<template #default="{ row }">{{ getProvider(row.provider) }}</template>
</el-table-column>
<el-table-column
@ -97,7 +97,12 @@
</el-table-column>
<el-table-column :label="$t('commons.button.log')" width="80px">
<template #default="{ row }">
<el-button @click="openSSLLog(row)" link type="primary" v-if="row.provider != 'manual'">
<el-button
@click="openSSLLog(row)"
link
type="primary"
v-if="row.provider != 'manual' && row.provider !== 'fromMaster'"
>
{{ $t('website.check') }}
</el-button>
</template>
@ -224,7 +229,7 @@ const buttons = [
{
label: i18n.global.t('ssl.apply'),
disabled: function (row: Website.SSLDTO) {
return row.status === 'applying' || row.provider === 'manual';
return row.status === 'applying' || row.provider === 'manual' || row.provider === 'fromMaster';
},
click: function (row: Website.SSLDTO) {
if (row.provider === 'dnsManual') {
@ -248,6 +253,9 @@ const buttons = [
},
{
label: i18n.global.t('commons.button.edit'),
disabled: function (row: Website.SSLDTO) {
return row.provider === 'fromMaster';
},
click: function (row: Website.SSLDTO) {
onEdit(row);
},