feat: ca website upload (#14148)

This commit is contained in:
Yuki Guo
2026-10-23 18:12:57 +08:00
committed by GitHub
parent 1af237944d
commit ec998552b9
42 changed files with 1169 additions and 780 deletions
+64
View File
@@ -1,13 +1,16 @@
package v2
import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/gin-gonic/gin"
)
@@ -176,3 +179,64 @@ func (b *BaseApi) DownloadCAFile(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)
}
// @Tags Website CA
// @Summary Upload website CA archive
// @Accept multipart/form-data
// @Param file formData file true "ZIP containing ca.crt and ca.key"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ca/upload [post]
func (b *BaseApi) UploadWebsiteCA(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 11<<20)
if err := c.Request.ParseMultipartForm(11 << 20); err != nil {
helper.InternalServer(c, buserr.New("ErrCAUpload"))
return
}
if c.Request.MultipartForm != nil {
defer c.Request.MultipartForm.RemoveAll()
}
file, err := c.FormFile("file")
if err != nil || file.Size > 10<<20 || !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") {
helper.InternalServer(c, buserr.New("ErrCAUpload"))
return
}
stream, err := file.Open()
if err != nil {
helper.InternalServer(c, err)
return
}
defer stream.Close()
data, err := io.ReadAll(io.LimitReader(stream, (10<<20)+1))
if err != nil {
helper.InternalServer(c, err)
return
}
if err = websiteCAService.Upload(file.Filename[:len(file.Filename)-len(".zip")], data); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website CA
// @Summary Import website CA from PEM or server files
// @Accept json
// @Param request body request.WebsiteCAImport true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ca/import [post]
func (b *BaseApi) ImportWebsiteCA(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 3<<20)
var req request.WebsiteCAImport
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteCAService.Import(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+9
View File
@@ -182,3 +182,12 @@ type WebsiteSSLFileUpload struct {
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
type WebsiteCAImport struct {
Name string `json:"name" validate:"required,max=128"`
Type string `json:"type" validate:"required,oneof=paste local"`
Certificate string `json:"certificate" validate:"required_if=Type paste"`
PrivateKey string `json:"privateKey" validate:"required_if=Type paste"`
CertificatePath string `json:"certificatePath" validate:"required_if=Type local"`
PrivateKeyPath string `json:"privateKeyPath" validate:"required_if=Type local"`
}
+157 -4
View File
@@ -1,15 +1,19 @@
package service
import (
"archive/zip"
"bytes"
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io"
"log"
"math/big"
"net"
@@ -18,25 +22,27 @@ import (
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/ssl"
"github.com/go-acme/lego/v5/certcrypto"
"gorm.io/gorm"
)
type WebsiteCAService struct {
}
type IWebsiteCAService interface {
Import(req request.WebsiteCAImport) error
Upload(name string, archive []byte) error
Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error)
Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error)
GetCA(id uint) (*response.WebsiteCADTO, error)
@@ -334,7 +340,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
websiteSSL.ExpireDate = cert.NotAfter
websiteSSL.StartDate = cert.NotBefore
websiteSSL.Type = cert.Issuer.CommonName
websiteSSL.Organization = rootCsr.Subject.Organization[0]
websiteSSL.Organization = strings.Join(rootCsr.Subject.Organization, ",")
websiteSSL.Status = constant.SSLReady
if req.Renew {
@@ -442,3 +448,150 @@ func (w WebsiteCAService) DownloadFile(id uint) (*os.File, error) {
}
return os.Open(path.Join(dir, fileName))
}
func (w WebsiteCAService) Upload(name string, archive []byte) error {
ca, err := parseCAArchive(archive)
if err != nil {
return err
}
return saveImportedCA(name, ca)
}
func saveImportedCA(name string, ca *model.WebsiteCA) error {
name = strings.TrimSpace(name)
if name == "" || len(name) > 128 || name == "." || name == ".." || strings.ContainsAny(name, "/\\\x00") {
return buserr.WithDetail("ErrInvalidParams", "name", nil)
}
exist, err := websiteCARepo.GetFirst(repo.WithByName(name))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if exist.ID > 0 {
return buserr.New("ErrNameIsExist")
}
ca.Name = name
return websiteCARepo.Create(context.Background(), ca)
}
// Read only the certificate and key in memory; never extract user-supplied paths.
func parseCAArchive(archive []byte) (*model.WebsiteCA, error) {
invalid := func() (*model.WebsiteCA, error) { return nil, buserr.New("ErrCAUpload") }
if len(archive) > 10<<20 {
return invalid()
}
reader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
if err != nil || len(reader.File) > 16 {
return invalid()
}
contents := make(map[string][]byte)
for _, file := range reader.File {
if file.FileInfo().IsDir() {
continue
}
name := path.Base(file.Name)
if (name != "ca.crt" && name != "ca.key") || contents[name] != nil || file.UncompressedSize64 > 1<<20 {
return invalid()
}
stream, err := file.Open()
if err != nil {
return invalid()
}
data, err := io.ReadAll(io.LimitReader(stream, (1<<20)+1))
stream.Close()
if err != nil || len(data) > 1<<20 {
return invalid()
}
contents[name] = data
}
return parseAndValidateRootCA(contents["ca.crt"], contents["ca.key"])
}
func parseAndValidateRootCA(certificate, privateKey []byte) (*model.WebsiteCA, error) {
invalid := func() (*model.WebsiteCA, error) { return nil, buserr.New("ErrCAImport") }
if len(certificate) > 1<<20 || len(privateKey) > 1<<20 {
return invalid()
}
pair, err := tls.X509KeyPair(certificate, privateKey)
if err != nil || len(pair.Certificate) != 1 {
return invalid()
}
cert := pair.Leaf
now := time.Now()
if cert == nil || !cert.IsCA || !cert.BasicConstraintsValid || cert.KeyUsage&x509.KeyUsageCertSign == 0 ||
!bytes.Equal(cert.RawIssuer, cert.RawSubject) || cert.CheckSignatureFrom(cert) != nil ||
now.Before(cert.NotBefore) || !now.Before(cert.NotAfter) {
return invalid()
}
ca := &model.WebsiteCA{CSR: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))}
// Normalize keys to the formats used by ObtainSSL, including PKCS#8 uploads.
switch key := pair.PrivateKey.(type) {
case *ecdsa.PrivateKey:
bits := key.Curve.Params().BitSize
if bits != 256 && bits != 384 {
return invalid()
}
der, err := x509.MarshalECPrivateKey(key)
if err != nil {
return invalid()
}
ca.KeyType = fmt.Sprintf("EC%d", bits)
ca.PrivateKey = string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}))
case *rsa.PrivateKey:
bits := key.N.BitLen()
if bits != 2048 && bits != 3072 && bits != 4096 && bits != 8192 {
return invalid()
}
ca.KeyType = fmt.Sprintf("RSA%d", bits)
ca.PrivateKey = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}))
default:
return invalid()
}
return ca, nil
}
func (w WebsiteCAService) Import(req request.WebsiteCAImport) error {
certificate, privateKey := []byte(req.Certificate), []byte(req.PrivateKey)
switch req.Type {
case "local":
var err error
certificate, err = readCAImportFile(req.CertificatePath)
if err != nil {
return err
}
privateKey, err = readCAImportFile(req.PrivateKeyPath)
if err != nil {
return err
}
case "paste":
default:
return buserr.New("ErrCAImport")
}
ca, err := parseAndValidateRootCA(certificate, privateKey)
if err != nil {
return err
}
return saveImportedCA(req.Name, ca)
}
func readCAImportFile(name string) ([]byte, error) {
info, err := os.Stat(name)
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() || info.Size() > 1<<20 {
return nil, buserr.New("ErrCAImport")
}
file, err := os.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
content, err := io.ReadAll(io.LimitReader(file, (1<<20)+1))
if err != nil {
return nil, err
}
if len(content) > 1<<20 {
return nil, buserr.New("ErrCAImport")
}
return content, nil
}
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "Recover snapshot [{{ .snapshot }}] for virtual machine [
VMSnapshotRecoverStep: "Recover virtual machine snapshot [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Delete snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
VMSnapshotDeleteStep: "Delete virtual machine snapshot [{{ .snapshot }}]"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "Restaurar instantánea [{{ .snapshot }}] para la máquin
VMSnapshotRecoverStep: "Restaurar instantánea de máquina virtual [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Eliminar instantánea [{{ .snapshot }}] de la máquina virtual [{{ .vm }}]"
VMSnapshotDeleteStep: "Eliminar instantánea de máquina virtual [{{ .snapshot }}]"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -774,3 +774,7 @@ VMSnapshotRecoverTask: "بازیابی اسنپ‌شات [{{ .snapshot }}] بر
VMSnapshotRecoverStep: "بازیابی اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
VMSnapshotDeleteTask: "حذف اسنپ‌شات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
VMSnapshotDeleteStep: "حذف اسنپ‌شات ماشین مجازی [{{ .snapshot }}]"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "仮想マシン [{{ .vm }}] のスナップショット
VMSnapshotRecoverStep: "仮想マシンのスナップショット [{{ .snapshot }}] を復元"
VMSnapshotDeleteTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を削除"
VMSnapshotDeleteStep: "仮想マシンのスナップショット [{{ .snapshot }}] を削除"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}]
VMSnapshotRecoverStep: "가상 머신 스냅샷 [{{ .snapshot }}] 복구"
VMSnapshotDeleteTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 삭제"
VMSnapshotDeleteStep: "가상 머신 스냅샷 [{{ .snapshot }}] 삭제"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -764,3 +764,7 @@ VMSnapshotRecoverTask: "ກູ້ຄືນສະແນັບຊັອດ [{{ .sn
VMSnapshotRecoverStep: "ກູ້ຄືນສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
VMSnapshotDeleteTask: "ລຶບສະແນັບຊັອດ [{{ .snapshot }}] ຂອງເຄື່ອງສະເໝືອນ [{{ .vm }}]"
VMSnapshotDeleteStep: "ລຶບສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "Pulihkan syot kilat [{{ .snapshot }}] untuk mesin maya [
VMSnapshotRecoverStep: "Pulihkan syot kilat mesin maya [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Padam syot kilat [{{ .snapshot }}] untuk mesin maya [{{ .vm }}]"
VMSnapshotDeleteStep: "Padam syot kilat mesin maya [{{ .snapshot }}]"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "Restaurar snapshot [{{ .snapshot }}] da máquina virtual
VMSnapshotRecoverStep: "Restaurar snapshot da máquina virtual [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Excluir snapshot [{{ .snapshot }}] da máquina virtual [{{ .vm }}]"
VMSnapshotDeleteStep: "Excluir snapshot da máquina virtual [{{ .snapshot }}]"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "Восстановить снимок [{{ .snapshot }}]
VMSnapshotRecoverStep: "Восстановить снимок виртуальной машины [{{ .snapshot }}]"
VMSnapshotDeleteTask: "Удалить снимок [{{ .snapshot }}] виртуальной машины [{{ .vm }}]"
VMSnapshotDeleteStep: "Удалить снимок виртуальной машины [{{ .snapshot }}]"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "Sanal makine [{{ .vm }}] anlık görüntüsü [{{ .snaps
VMSnapshotRecoverStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] geri yükle"
VMSnapshotDeleteTask: "Sanal makine [{{ .vm }}] anlık görüntüsü [{{ .snapshot }}] sil"
VMSnapshotDeleteStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] sil"
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "復原虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotRecoverStep: "復原虛擬機快照 [{{ .snapshot }}]"
VMSnapshotDeleteTask: "刪除虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotDeleteStep: "刪除虛擬機快照 [{{ .snapshot }}]"
ErrCAUpload: "請上傳包含有效自簽根憑證 ca.crt 及匹配私鑰 ca.key 的 ZIP 檔案(最大 10 MB),金鑰演算法須為支援的 RSA 或 EC。"
ErrCAImport: "請提供有效的自簽根憑證及匹配私鑰(每個最大 1 MB),金鑰演算法須為支援的 RSA 或 EC。"
+4
View File
@@ -773,3 +773,7 @@ VMSnapshotRecoverTask: "恢复虚拟机 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotRecoverStep: "恢复虚拟机快照 [{{ .snapshot }}]"
VMSnapshotDeleteTask: "删除虚拟机 [{{ .vm }}] 快照 [{{ .snapshot }}]"
VMSnapshotDeleteStep: "删除虚拟机快照 [{{ .snapshot }}]"
ErrCAUpload: "请上传包含有效自签根证书 ca.crt 及匹配私钥 ca.key 的 ZIP 文件(最大 10 MB),密钥算法须为支持的 RSA 或 EC。"
ErrCAImport: "请提供有效的自签根证书及匹配私钥(每个最大 1 MB),密钥算法须为支持的 RSA 或 EC。"
+2
View File
@@ -13,6 +13,8 @@ func (a *WebsiteCARouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v2.ApiGroupApp.BaseApi
{
groupRouter.POST("/import", baseApi.ImportWebsiteCA)
groupRouter.POST("/upload", baseApi.UploadWebsiteCA)
groupRouter.POST("/search", baseApi.PageWebsiteCA)
groupRouter.POST("", baseApi.CreateWebsiteCA)
groupRouter.POST("/del", baseApi.DeleteWebsiteCA)
+3
View File
@@ -626,6 +626,9 @@ export namespace Website {
}
export interface SSLObtainByCA {
autoRenew: boolean;
execShell: boolean;
shell: string;
id: number;
domains: string;
keyType: string;
+15
View File
@@ -451,3 +451,18 @@ export const deleteTemplateOutput = (params: { id: number }) => {
export const getTemplateOutput = (id: number) => {
return http.post<Website.TemplateOutputDTO>(`/websites/templates/outputs/get`, { id });
};
export const uploadCA = (params: FormData) => {
return http.upload(`/websites/ca/upload`, params);
};
export const importCA = (params: {
name: string;
type: 'paste' | 'local';
certificate?: string;
privateKey?: string;
certificatePath?: string;
privateKeyPath?: string;
}) => {
return http.post(`/websites/ca/import`, params);
};
+10 -2
View File
@@ -1,6 +1,11 @@
<template>
<div>
<DialogPro v-model="open" :title="form.title" size="small" @close="handleClose">
<component
:is="drawer ? DrawerPro : DialogPro"
v-model="open"
v-bind="drawer ? { header: form.title, size: 'large' } : { title: form.title, size: 'small' }"
@close="handleClose"
>
<div v-loading="loading" class="del-dialog-body">
<el-row type="flex" justify="center">
<el-col :span="22">
@@ -30,11 +35,14 @@
</el-button>
</span>
</template>
</DialogPro>
</component>
</div>
</template>
<script setup lang="ts">
import DialogPro from '@/components/dialog-pro/index.vue';
import DrawerPro from '@/components/drawer-pro/index.vue';
withDefaults(defineProps<{ drawer?: boolean }>(), { drawer: false });
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { reactive, ref } from 'vue';
+1
View File
@@ -4376,6 +4376,7 @@ const message = {
'Enabling this can avoid certificate leakage, setting a default website will invalidate this setting',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm:
'The following certificates will be deleted. ACME certificates will be automatically revoked. Continue?',
deleteAndRevoke: 'Delete and revoke',
+1
View File
@@ -4429,6 +4429,7 @@ const message = {
'Habilitar esto puede evitar la fuga de certificados, establecer un sitio web predeterminado invalidará esta configuración',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm:
'Se eliminarán los siguientes certificados. Los certificados ACME se revocarán automáticamente. ¿Desea continuar?',
deleteAndRevoke: 'Eliminar y revocar',
+1
View File
@@ -4338,6 +4338,7 @@ const message = {
'فعال‌سازی این گزینه می‌تواند از نشت گواهی جلوگیری کند، تنظیم یک وب‌سایت پیش‌فرض این تنظیم را بی‌اثر می‌کند',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm: 'گواهی‌های زیر حذف خواهند شد. گواهی‌های ACME به‌طور خودکار باطل خواهند شد. آیا ادامه می‌دهید؟',
deleteAndRevoke: 'حذف و ابطال',
create: 'درخواست',
+1
View File
@@ -4361,6 +4361,7 @@ const message = {
'有効にすると証明書の漏洩を防げますが、デフォルト Web サイトを設定するとこの設定は無効になります',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm: '以下の証明書を削除します。ACME 証明書は自動的に失効されます。続行しますか?',
deleteAndRevoke: '削除して失効',
create: 'リクエスト',
+1
View File
@@ -4281,6 +4281,7 @@ const message = {
'활성화하면 인증서 누출을 방지할 수 있지만, 기본 웹사이트를 설정하면 이 설정이 무효화됩니다',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm: '다음 인증서를 삭제합니다. ACME 인증서는 자동으로 폐기됩니다. 계속하시겠습니까?',
deleteAndRevoke: '삭제 및 폐기',
create: '요청',
+1
View File
@@ -4250,6 +4250,7 @@ const message = {
'ການເປີດໃຊ້ສິ່ງນີ້ສາມາດຫຼີກລ່ຽງການຮົ່ວໄຫຼຂອງໃບຢັ້ງຢືນ, ແຕ່ການຕັ້ງເວັບໄຊເລີ່ມຕົ້ນຈະເຮັດໃຫ້ການຕັ້ງຄ່ານີ້ບໍ່ມີຜົນ',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm: 'ຈະລຶບໃບຢັ້ງຢືນຕໍ່ໄປນີ້. ໃບຢັ້ງຢືນ ACME ຈະຖືກຖອນໂດຍອັດຕະໂນມັດ. ຕ້ອງການສືບຕໍ່ບໍ?',
deleteAndRevoke: 'ລຶບ ແລະ ຖອນ',
create: 'ຮ້ອງຂໍ',
+1
View File
@@ -4443,6 +4443,7 @@ const message = {
'Mengaktifkan ini boleh mengelakkan kebocoran sijil, menetapkan laman web lalai akan membatalkan tetapan ini',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm: 'Sijil berikut akan dipadamkan. Sijil ACME akan dibatalkan secara automatik. Teruskan?',
deleteAndRevoke: 'Padam dan batalkan',
create: 'Permintaan',
+1
View File
@@ -4461,6 +4461,7 @@ const message = {
'Ativar isso pode evitar vazamento de certificados, definir um site padrão invalidará esta configuração',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm:
'Os certificados a seguir serão excluídos. Os certificados ACME serão revogados automaticamente. Continuar?',
deleteAndRevoke: 'Excluir e revogar',
+1
View File
@@ -4433,6 +4433,7 @@ const message = {
'Включение этого может предотвратить утечку сертификатов, установка веб-сайта по умолчанию сделает эту настройку недействительной',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm:
'Следующие сертификаты будут удалены. Сертификаты ACME будут автоматически отозваны. Продолжить?',
deleteAndRevoke: 'Удалить и отозвать',
+1
View File
@@ -4447,6 +4447,7 @@ const message = {
'Etkinleştirilmesi sertifika sızıntısını önleyebilir, varsayılan bir web sitesi ayarlamak bu ayarı geçersiz kılar',
},
ssl: {
caUploadHelp: 'Upload the downloaded ZIP containing ca.crt and ca.key (up to 10 MB).',
deleteConfirm:
'Aşağıdaki sertifikalar silinecek. ACME sertifikaları otomatik olarak iptal edilecek. Devam edilsin mi?',
deleteAndRevoke: 'Sil ve iptal et',
+1
View File
@@ -4080,6 +4080,7 @@ const message = {
sslRejectHandshakeHelper: '開啟之後可以避免憑證洩露,設置預設網站會讓此設置失效',
},
ssl: {
caUploadHelp: '上傳下載的自簽根憑證 ZIP 檔案,包含 ca.crt 和 ca.key,最大 10 MB。',
deleteConfirm: '將對以下憑證進行 刪除 操作,ACME 類型憑證會被自動撤銷,是否繼續?',
deleteAndRevoke: '刪除並撤銷',
create: '申請憑證',
+1
View File
@@ -4125,6 +4125,7 @@ const message = {
sslRejectHandshakeHelper: '开启之后可以避免证书泄露,设置默认网站会让此设置失效',
},
ssl: {
caUploadHelp: '上传下载的自签根证书 ZIP 文件,包含 ca.crt 和 ca.key,最大 10 MB。',
deleteConfirm: '将对以下证书进行 删除 操作,ACME类型证书会被自动吊销,是否继续?',
deleteAndRevoke: '删除并吊销',
create: '申请证书',
+36
View File
@@ -43,6 +43,42 @@ const webSiteRouter = {
permission: 'website_cert_view',
},
},
{
path: '/websites/ssl/ca',
name: 'SSL-CA',
component: () => import('@/views/website/ssl/ca/index.vue'),
hidden: true,
meta: {
activeMenu: '/websites/ssl',
title: 'menu.ssl',
detail: 'ssl.selfSigned',
permission: 'website_cert_view',
},
},
{
path: '/websites/ssl/acme-account',
name: 'SSL-ACME',
component: () => import('@/views/website/ssl/acme-account/index.vue'),
hidden: true,
meta: {
activeMenu: '/websites/ssl',
title: 'menu.ssl',
detail: 'website.acmeAccountManage',
permission: 'website_cert_view',
},
},
{
path: '/websites/ssl/dns-account',
name: 'SSL-DNS',
component: () => import('@/views/website/ssl/dns-account/index.vue'),
hidden: true,
meta: {
activeMenu: '/websites/ssl',
title: 'menu.ssl',
detail: 'website.dnsAccountManage',
permission: 'website_cert_view',
},
},
{
path: '/websites/templates',
name: 'WebsiteTemplate',
@@ -1,78 +1,81 @@
<template>
<DialogPro v-model="open" :title="$t('commons.button.create')" size="small" @close="handleClose">
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form @submit.prevent ref="accountForm" label-position="top" :model="account" :rules="rules">
<el-form-item :label="$t('website.email')" prop="email">
<el-input v-model.trim="account.email"></el-input>
</el-form-item>
<el-form-item :label="$t('website.useProxy')" prop="useProxy" v-if="isProductPro">
<el-switch v-model="account.useProxy"></el-switch>
<span class="input-help">
{{ $t('website.useProxyHelper') }}
</span>
</el-form-item>
<el-form-item :label="$t('website.acmeAccountType')" prop="type">
<el-select v-model="account.type">
<el-option
v-for="(acme, index) in AcmeAccountTypes"
:key="index"
:label="acme.label"
:value="acme.value"
></el-option>
</el-select>
<span class="input-help" v-if="account.type === 'buypass'">
{{ $t('ssl.buypassHelper') }}
</span>
<span class="input-help" v-if="account.type == 'google'">
{{ $t('ssl.googleCloudHelper') }}
</span>
</el-form-item>
<el-form-item :label="$t('website.keyType')" prop="keyType">
<el-select v-model="account.keyType">
<el-option
v-for="(keyType, index) in KeyTypes"
:key="index"
:label="keyType.label"
:value="keyType.value"
></el-option>
</el-select>
</el-form-item>
<div v-if="account.type == 'custom'">
<el-form-item :label="$t('ssl.customAcmeURL')" prop="caDirURL">
<el-input v-model.trim="account.caDirURL"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.useEAB')" prop="useEAB">
<el-switch v-model="account.useEAB"></el-switch>
</el-form-item>
</div>
<DrawerPro v-model="open" :header="$t('commons.button.create')" size="large" @close="handleClose">
<el-form
v-loading="loading"
@submit.prevent
ref="accountForm"
label-position="top"
:model="account"
:rules="rules"
>
<el-form-item :label="$t('website.email')" prop="email">
<el-input v-model.trim="account.email"></el-input>
</el-form-item>
<el-form-item :label="$t('website.useProxy')" prop="useProxy" v-if="isProductPro">
<el-switch v-model="account.useProxy"></el-switch>
<span class="input-help">
{{ $t('website.useProxyHelper') }}
</span>
</el-form-item>
<el-form-item :label="$t('website.acmeAccountType')" prop="type">
<el-select v-model="account.type">
<el-option
v-for="(acme, index) in AcmeAccountTypes"
:key="index"
:label="acme.label"
:value="acme.value"
></el-option>
</el-select>
<span class="input-help" v-if="account.type === 'buypass'">
{{ $t('ssl.buypassHelper') }}
</span>
<span class="input-help" v-if="account.type == 'google'">
{{ $t('ssl.googleCloudHelper') }}
</span>
</el-form-item>
<el-form-item :label="$t('website.keyType')" prop="keyType">
<el-select v-model="account.keyType">
<el-option
v-for="(keyType, index) in KeyTypes"
:key="index"
:label="keyType.label"
:value="keyType.value"
></el-option>
</el-select>
</el-form-item>
<div v-if="account.type == 'custom'">
<el-form-item :label="$t('ssl.customAcmeURL')" prop="caDirURL">
<el-input v-model.trim="account.caDirURL"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.useEAB')" prop="useEAB">
<el-switch v-model="account.useEAB"></el-switch>
</el-form-item>
</div>
<div
v-if="
account.type == 'google' ||
account.type == 'freessl' ||
(account.type === 'custom' && account.useEAB)
"
>
<el-form-item label="EAB kid" prop="eabKid">
<el-input v-model.trim="account.eabKid"></el-input>
</el-form-item>
<el-form-item label="EAB HmacKey" prop="eabHmacKey">
<el-input type="textarea" :rows="3" v-model.trim="account.eabHmacKey"></el-input>
</el-form-item>
<el-link
v-if="account.type == 'google'"
class="ml-1.5"
type="primary"
target="_blank"
href="https://cloud.google.com/certificate-manager/docs/public-ca-tutorial?hl=zh-cn"
>
{{ $t('ssl.googleHelper') }}
</el-link>
</div>
</el-form>
</el-col>
</el-row>
<div
v-if="
account.type == 'google' ||
account.type == 'freessl' ||
(account.type === 'custom' && account.useEAB)
"
>
<el-form-item label="EAB kid" prop="eabKid">
<el-input v-model.trim="account.eabKid"></el-input>
</el-form-item>
<el-form-item label="EAB HmacKey" prop="eabHmacKey">
<el-input type="textarea" :rows="3" v-model.trim="account.eabHmacKey"></el-input>
</el-form-item>
<el-link
v-if="account.type == 'google'"
class="ml-1.5"
type="primary"
target="_blank"
href="https://cloud.google.com/certificate-manager/docs/public-ca-tutorial?hl=zh-cn"
>
{{ $t('ssl.googleHelper') }}
</el-link>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
@@ -81,7 +84,7 @@
</el-button>
</span>
</template>
</DialogPro>
</DrawerPro>
</template>
<script lang="ts" setup>
import { FormInstance } from 'element-plus';
@@ -118,16 +121,15 @@ const initData = () => ({
});
const account = ref(initData());
const em = defineEmits(['close']);
const em = defineEmits(['saved']);
const handleClose = () => {
resetForm();
open.value = false;
em('close', false);
};
const resetForm = () => {
accountForm.value.resetFields();
accountForm.value?.resetFields();
account.value = initData();
};
@@ -136,22 +138,17 @@ const acceptParams = () => {
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
createAcmeAccount(account.value)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
});
if (!formEl || loading.value) return;
loading.value = true;
try {
if (!(await formEl.validate().catch(() => false))) return;
await createAcmeAccount(account.value);
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
em('saved');
} finally {
loading.value = false;
}
};
defineExpose({
@@ -1,50 +1,53 @@
<template>
<DrawerPro v-model="open" :header="$t('website.acmeAccountManage')" size="large" @close="handleClose">
<template #content>
<div class="mb-1.5">
<div>
<RouterButton :buttons="routerButton" />
<LayoutContent backName="SSL" :title="$t('website.acmeAccountManage')">
<template #leftToolBar>
<el-button v-permission type="primary" @click="openCreate">
{{ $t('commons.button.create') }}
</el-button>
</template>
<template #prompt>
<el-alert :title="$t('ssl.acmeHelper')" type="info" :closable="false" />
</div>
<ComplexTable :data="data" :pagination-config="paginationConfig" v-loading="loading">
<template #toolbar>
<el-button v-permission type="primary" @click="openCreate">
{{ $t('commons.button.create') }}
</el-button>
</template>
<el-table-column
:label="$t('website.email')"
fix
show-overflow-tooltip
prop="email"
min-width="100px"
></el-table-column>
<el-table-column :label="$t('website.acmeAccountType')" fix show-overflow-tooltip prop="type">
<template #default="{ row }">
{{ getAccountName(row.type) }}
</template>
</el-table-column>
<el-table-column :label="$t('website.keyType')" prop="keyType">
<template #default="{ row }">
{{ getKeyName(row.keyType) }}
</template>
</el-table-column>
<el-table-column :label="$t('website.useProxy')" min-width="100px" v-if="isProductPro">
<template #default="{ row }">
<el-switch v-permission v-model="row.useProxy" @change="update(row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="URL" show-overflow-tooltip prop="url" min-width="300px"></el-table-column>
<fu-table-operations
:ellipsis="1"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
<Create ref="createRef" @close="search()"></Create>
</template>
</DrawerPro>
<OpDialog ref="opRef" @search="search" />
</template>
<template #main>
<ComplexTable :data="data" :pagination-config="paginationConfig" @search="search()" v-loading="loading">
<el-table-column
:label="$t('website.email')"
fix
show-overflow-tooltip
prop="email"
min-width="100px"
></el-table-column>
<el-table-column :label="$t('website.acmeAccountType')" fix show-overflow-tooltip prop="type">
<template #default="{ row }">
{{ getAccountName(row.type) }}
</template>
</el-table-column>
<el-table-column :label="$t('website.keyType')" prop="keyType">
<template #default="{ row }">
{{ getKeyName(row.keyType) }}
</template>
</el-table-column>
<el-table-column :label="$t('website.useProxy')" min-width="100px" v-if="isProductPro">
<template #default="{ row }">
<el-switch v-permission v-model="row.useProxy" @change="update(row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="URL" show-overflow-tooltip prop="url" min-width="300px"></el-table-column>
<fu-table-operations
:ellipsis="1"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<Create ref="createRef" @saved="search()" />
<OpDialog drawer ref="opRef" @search="search" />
</div>
</template>
<script lang="ts" setup>
@@ -52,14 +55,14 @@ import { readCachedPageSize } from '@/utils/panel-cache';
import { Website } from '@/api/interface/website';
import { deleteAcmeAccount, searchAcmeAccount, updateAcmeAccount } from '@/api/modules/website';
import i18n from '@/lang';
import { reactive, ref } from 'vue';
import { useSearchReset } from '@/composables/useSearchReset';
import { onMounted, reactive, ref, toRef } from 'vue';
import Create from './create/index.vue';
import { getAccountName, getKeyName } from '@/utils/ssl';
import { MsgSuccess } from '@/utils/message';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isProductPro } = useGlobalStore();
const open = ref(false);
const loading = ref(false);
const data = ref();
const createRef = ref();
@@ -81,11 +84,6 @@ const buttons = [
},
];
const acceptParams = () => {
search();
open.value = true;
};
const search = async () => {
const req = {
page: paginationConfig.currentPage,
@@ -108,10 +106,6 @@ const openCreate = () => {
createRef.value.acceptParams();
};
const handleClose = () => {
open.value = false;
};
const deleteAccount = async (row: any) => {
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
@@ -125,7 +119,9 @@ const deleteAccount = async (row: any) => {
});
};
defineExpose({
acceptParams,
const routerButton = [{ label: i18n.global.t('website.acmeAccountManage'), path: '/websites/ssl/acme-account' }];
useSearchReset({ page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
onMounted(() => {
search();
});
</script>
@@ -1,42 +1,38 @@
<template>
<DialogPro v-model="open" :title="$t('commons.button.create')" @close="handleClose">
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form @submit.prevent ref="caForm" label-position="top" :model="ca" :rules="rules">
<el-form-item :label="$t('ssl.caName')" prop="name">
<el-input v-model.trim="ca.name"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.commonName')" prop="commonName">
<el-input v-model.trim="ca.commonName"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.company')" prop="organization">
<el-input v-model.trim="ca.organization"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.department')" prop="organizationUint">
<el-input v-model.trim="ca.organizationUint"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.country')" prop="country">
<el-input v-model.trim="ca.country"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.province')" prop="province">
<el-input v-model.trim="ca.province"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.city')" prop="city">
<el-input v-model.trim="ca.city"></el-input>
</el-form-item>
<el-form-item :label="$t('website.keyType')" prop="keyType">
<el-select v-model="ca.keyType">
<el-option
v-for="(keyType, index) in KeyTypes"
:key="index"
:label="keyType.label"
:value="keyType.value"
></el-option>
</el-select>
</el-form-item>
</el-form>
</el-col>
</el-row>
<DrawerPro v-model="open" size="large" :header="$t('commons.button.create')" @close="handleClose">
<el-form v-loading="loading" @submit.prevent ref="caForm" label-position="top" :model="ca" :rules="rules">
<el-form-item :label="$t('ssl.caName')" prop="name">
<el-input v-model.trim="ca.name"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.commonName')" prop="commonName">
<el-input v-model.trim="ca.commonName"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.company')" prop="organization">
<el-input v-model.trim="ca.organization"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.department')" prop="organizationUint">
<el-input v-model.trim="ca.organizationUint"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.country')" prop="country">
<el-input v-model.trim="ca.country"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.province')" prop="province">
<el-input v-model.trim="ca.province"></el-input>
</el-form-item>
<el-form-item :label="$t('ssl.city')" prop="city">
<el-input v-model.trim="ca.city"></el-input>
</el-form-item>
<el-form-item :label="$t('website.keyType')" prop="keyType">
<el-select v-model="ca.keyType">
<el-option
v-for="(keyType, index) in KeyTypes"
:key="index"
:label="keyType.label"
:value="keyType.value"
></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
@@ -45,7 +41,7 @@
</el-button>
</span>
</template>
</DialogPro>
</DrawerPro>
</template>
<script lang="ts" setup>
@@ -90,7 +86,7 @@ const handleClose = () => {
};
const resetForm = () => {
caForm.value.resetFields();
caForm.value?.resetFields();
ca.value = initData();
};
+53 -61
View File
@@ -1,38 +1,48 @@
<template>
<DrawerPro v-model="open" :header="$t('ssl.selfSigned')" size="large" @close="handleClose">
<template #content>
<ComplexTable :data="data" :pagination-config="paginationConfig" @search="search()" v-loading="loading">
<template #toolbar>
<el-button v-permission type="primary" @click="openCreate">
{{ $t('commons.button.create') }}
</el-button>
</template>
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name"></el-table-column>
<el-table-column :label="$t('website.keyType')" show-overflow-tooltip prop="keyType">
<template #default="{ row }">
{{ getKeyName(row.keyType) }}
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
/>
<fu-table-operations
:ellipsis="3"
:buttons="buttons"
:label="$t('commons.table.operate')"
fix
width="250px"
/>
</ComplexTable>
<Create ref="createRef" @close="search()" />
<Obtain ref="obtainRef" @close="search()" />
<Detail ref="detailRef" />
</template>
</DrawerPro>
<OpDialog ref="opRef" @search="search" />
<div>
<RouterButton :buttons="routerButton" />
<LayoutContent backName="SSL" :title="$t('ssl.selfSigned')">
<template #leftToolBar>
<el-button v-permission type="primary" @click="openCreate">
{{ $t('commons.button.create') }}
</el-button>
<el-button v-permission type="primary" plain @click="uploadRef?.acceptParams()">
{{ $t('commons.button.upload') }}
</el-button>
</template>
<template #main>
<ComplexTable :data="data" :pagination-config="paginationConfig" @search="search()" v-loading="loading">
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
prop="name"
></el-table-column>
<el-table-column :label="$t('website.keyType')" show-overflow-tooltip prop="keyType">
<template #default="{ row }">
{{ getKeyName(row.keyType) }}
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
/>
<fu-table-operations
:ellipsis="3"
:buttons="buttons"
:label="$t('commons.table.operate')"
fix
width="250px"
/>
</ComplexTable>
</template>
</LayoutContent>
<Upload ref="uploadRef" @uploaded="search()" />
<Create ref="createRef" @close="search()" />
<Detail ref="detailRef" />
<OpDialog ref="opRef" @search="search" />
</div>
</template>
<script lang="ts" setup>
@@ -40,17 +50,19 @@ import { readCachedPageSize } from '@/utils/panel-cache';
import { Website } from '@/api/interface/website';
import { deleteCA, searchCAs, downloadCAFile } from '@/api/modules/website';
import i18n from '@/lang';
import { reactive, ref } from 'vue';
import { onMounted, reactive, ref, toRef } from 'vue';
import { useSearchReset } from '@/composables/useSearchReset';
import Create from './create/index.vue';
import Upload from './upload/index.vue';
import Detail from './detail/index.vue';
import { getKeyName } from '@/utils/ssl';
import { dateFormat } from '@/utils/date';
import Obtain from './obtain/index.vue';
const open = ref(false);
const routerButton = [{ label: i18n.global.t('ssl.selfSigned'), path: '/websites/ssl/ca' }];
const loading = ref(false);
const data = ref();
const createRef = ref();
const uploadRef = ref();
const paginationConfig = reactive({
cacheSizeKey: 'ca-page-size',
currentPage: 1,
@@ -58,18 +70,9 @@ const paginationConfig = reactive({
total: 0,
});
const opRef = ref();
const obtainRef = ref();
const em = defineEmits(['close']);
const detailRef = ref();
const buttons = [
{
label: i18n.global.t('ssl.selfSign'),
permission: true,
click: function (row: Website.CA) {
obtain(row);
},
},
{
label: i18n.global.t('ssl.detail'),
click: function (row: Website.CA) {
@@ -91,14 +94,7 @@ const buttons = [
},
];
const acceptParams = () => {
search();
open.value = true;
};
const obtain = (row: any) => {
obtainRef.value.acceptParams(row.id);
};
useSearchReset({ page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async () => {
const req = {
@@ -115,11 +111,6 @@ const openCreate = () => {
createRef.value.acceptParams();
};
const handleClose = () => {
em('close', false);
open.value = false;
};
const deleteca = async (row: any) => {
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
@@ -144,13 +135,14 @@ const onDownload = (row: Website.CA) => {
a.download = row.name + '.zip';
const event = new MouseEvent('click');
a.dispatchEvent(event);
window.URL.revokeObjectURL(downloadUrl);
})
.finally(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
onMounted(() => {
search();
});
</script>
@@ -1,194 +0,0 @@
<template>
<DialogPro v-model="open" :title="$t('ssl.selfSigned')" @close="handleClose">
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form @submit.prevent ref="obtainForm" label-position="top" :model="obtain" :rules="rules">
<el-form-item :label="$t('website.domain')" prop="domains">
<el-input
type="textarea"
:rows="4"
v-model="obtain.domains"
:placeholder="$t('ssl.domainHelper')"
></el-input>
</el-form-item>
<el-form-item :label="$t('website.remark')" prop="description">
<el-input v-model="obtain.description"></el-input>
</el-form-item>
<el-form-item :label="$t('website.keyType')" prop="keyType">
<el-select v-model="obtain.keyType">
<el-option
v-for="(keyType, index) in KeyTypes"
:key="index"
:label="keyType.label"
:value="keyType.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('ssl.days')" prop="time">
<el-input type="number" v-model.number="obtain.time">
<template #append>
<el-select v-model="obtain.unit" style="width: 100px">
<el-option :label="$t('commons.units.day')" value="day"></el-option>
<el-option :label="$t('commons.units.year')" value="year"></el-option>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item :label="''" prop="autoRenew">
<el-checkbox v-model="obtain.autoRenew" :label="$t('ssl.autoRenew')" />
</el-form-item>
<el-form-item :label="''" prop="pushDir">
<el-checkbox v-model="obtain.pushDir" :label="$t('ssl.pushDir')" />
</el-form-item>
<el-form-item :label="$t('ssl.dir')" prop="dir" v-if="obtain.pushDir">
<el-button
icon="Folder"
class="mb-2"
@click="
fileRef.acceptParams({
path: obtain.dir.split('\n')[0],
dir: true,
multiple: true,
selectedPaths: obtain.dir.split('\n').filter(Boolean),
})
"
>
{{ $t('ssl.dir') }}
</el-button>
<el-input v-model.trim="obtain.dir" type="textarea" :rows="3" />
<span class="input-help">
{{ $t('ssl.pushDirHelper') }}
</span>
</el-form-item>
<el-form-item :label="''" prop="execShell">
<el-checkbox v-model="obtain.execShell" :label="$t('ssl.execShell')" />
</el-form-item>
<el-form-item :label="$t('ssl.shell')" prop="shell" v-if="obtain.execShell">
<el-input type="textarea" :rows="4" v-model="obtain.shell" />
<span class="input-help">
{{ $t('ssl.shellHelper') }}
</span>
</el-form-item>
<PushToNode
v-if="isMaster && isXpackOrEE"
:push-node="obtain.pushNode"
:nodes="obtain.pushNodes"
type="ssl"
@update:push-node="obtain.pushNode = $event"
@update:nodes="obtain.pushNodes = $event"
/>
</el-form>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button v-permission type="primary" @click="submit(obtainForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DialogPro>
<FileList ref="fileRef" @choose="getPath" />
</template>
<script lang="ts" setup>
import { obtainSSLByCA } from '@/api/modules/website';
import { Rules, checkNumberRange, checkMaxLength } from '@/global/form-rules';
import i18n from '@/lang';
import FileList from '@/components/file-list/index.vue';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { defineAsyncComponent, ref } from 'vue';
import { KeyTypes } from '@/global/mimetype';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isMaster, isXpackOrEE } = 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 open = ref(false);
const fileRef = ref();
const loading = ref(false);
const obtainForm = ref<FormInstance>();
const em = defineEmits(['close']);
const rules = ref({
keyType: [Rules.requiredSelect],
domains: [Rules.requiredInput],
dir: [Rules.requiredInput],
time: [Rules.integerNumber, checkNumberRange(1, 10000)],
shell: [Rules.requiredInput],
description: [checkMaxLength(128)],
pushNodes: [Rules.requiredSelect],
});
const initData = () => ({
keyType: 'EC256',
domains: '',
id: 0,
time: 10,
unit: 'year',
pushDir: false,
dir: '',
autoRenew: true,
description: '',
execShell: false,
shell: '',
pushNode: false,
pushNodes: [] as string[],
nodes: '',
});
const obtain = ref(initData());
const acceptParams = (id: number) => {
open.value = true;
obtain.value.id = id;
};
const handleClose = () => {
open.value = false;
em('close', false);
resetForm();
};
const resetForm = () => {
obtainForm.value?.resetFields();
obtain.value = initData();
};
const getPath = (dirs: string[]) => {
obtain.value.dir = dirs.join('\n');
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
obtain.value.nodes = obtain.value.pushNode ? obtain.value.pushNodes.join(',') : '';
obtainSSLByCA(obtain.value)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>
@@ -0,0 +1,163 @@
<template>
<DrawerPro v-model="open" size="large" :header="$t('commons.button.upload')" @close="handleClose">
<el-form ref="formRef" label-position="top" :model="form" :rules="rules" :disabled="loading" @submit.prevent>
<el-form-item :label="$t('website.importType')" prop="type">
<el-select v-model="form.type" :disabled="loading" @change="changeType">
<el-option :label="$t('website.pasteSSL')" :value="'paste'"></el-option>
<el-option :label="$t('website.localSSL')" :value="'local'"></el-option>
<el-option :label="$t('commons.button.upload') + ' ZIP'" :value="'upload'"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="form.type !== 'upload'" :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="form.name" :maxlength="128" />
</el-form-item>
<div v-if="form.type === 'paste'">
<el-form-item :label="$t('website.certificate')" prop="certificate">
<el-input v-model="form.certificate" :rows="6" type="textarea" />
</el-form-item>
<el-form-item :label="$t('website.privateKey')" prop="privateKey">
<el-input v-model="form.privateKey" :rows="6" type="textarea" />
</el-form-item>
</div>
<div v-if="form.type === 'local'">
<el-form-item :label="$t('website.certificatePath')" prop="certificatePath">
<el-input v-model="form.certificatePath">
<template #prepend>
<el-button icon="Folder" @click="certFileRef.acceptParams({ dir: false })" />
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('website.privateKeyPath')" prop="privateKeyPath">
<el-input v-model="form.privateKeyPath">
<template #prepend>
<el-button icon="Folder" @click="keyFileRef.acceptParams({ dir: false })" />
</template>
</el-input>
</el-form-item>
</div>
<el-form-item v-if="form.type === 'upload'" :label="$t('file.file')" prop="file">
<el-upload
ref="uploadRef"
accept=".zip"
:auto-upload="false"
:limit="1"
:disabled="loading"
:on-change="onChange"
:on-remove="onRemove"
>
<el-button type="primary" icon="Upload" :disabled="loading">
{{ $t('file.selectFile') }}
</el-button>
<template #tip>
<div class="el-upload__tip">{{ $t('ssl.caUploadHelp') }}</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="loading" @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button v-permission type="primary" :loading="loading" @click="submit">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</DrawerPro>
<FileList ref="certFileRef" @choose="form.certificatePath = $event" />
<FileList ref="keyFileRef" @choose="form.privateKeyPath = $event" />
</template>
<script lang="ts" setup>
import { nextTick, reactive, ref } from 'vue';
import type { FormInstance, UploadFile, UploadInstance, UploadRawFile } from 'element-plus';
import { Rules } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue';
import { uploadCA, importCA } from '@/api/modules/website';
import { MsgError, MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
const open = ref(false);
const loading = ref(false);
const formRef = ref<FormInstance>();
const uploadRef = ref<UploadInstance>();
const certFileRef = ref();
const keyFileRef = ref();
const initialForm = () => ({
name: '',
type: 'upload',
certificate: '',
privateKey: '',
certificatePath: '',
privateKeyPath: '',
file: undefined as UploadRawFile | undefined,
});
const form = reactive(initialForm());
const rules = {
name: [Rules.requiredInput, Rules.name],
type: [Rules.requiredSelect],
file: [Rules.requiredSelect],
certificate: [Rules.requiredInput],
privateKey: [Rules.requiredInput],
certificatePath: [Rules.requiredInput],
privateKeyPath: [Rules.requiredInput],
};
const changeType = async () => {
form.file = undefined;
uploadRef.value?.clearFiles();
await nextTick();
formRef.value?.clearValidate();
};
const emit = defineEmits(['uploaded']);
const onRemove = () => {
form.file = undefined;
};
const onChange = (file: UploadFile) => {
if (!file.raw || !file.name.toLowerCase().endsWith('.zip') || file.raw.size > 10 * 1024 * 1024) {
uploadRef.value?.clearFiles();
form.file = undefined;
MsgError(i18n.global.t('ssl.caUploadHelp'));
return;
}
form.file = file.raw;
formRef.value?.clearValidate('file');
};
const handleClose = () => {
open.value = false;
};
const acceptParams = async () => {
Object.assign(form, initialForm());
open.value = true;
await nextTick();
uploadRef.value?.clearFiles();
formRef.value?.clearValidate();
};
const submit = async () => {
if (loading.value) return;
loading.value = true;
try {
if (!(await formRef.value?.validate().catch(() => false))) return;
if (form.type === 'upload') {
if (!form.file) return;
const data = new FormData();
data.append('file', form.file);
await uploadCA(data);
} else {
await importCA(
form.type === 'paste'
? { type: 'paste', name: form.name, certificate: form.certificate, privateKey: form.privateKey }
: {
type: 'local',
name: form.name,
certificatePath: form.certificatePath,
privateKeyPath: form.privateKeyPath,
},
);
}
MsgSuccess(i18n.global.t('commons.msg.uploadSuccess'));
handleClose();
emit('uploaded');
} finally {
loading.value = false;
}
};
defineExpose({ acceptParams });
</script>
+147 -14
View File
@@ -1,7 +1,33 @@
<template>
<DrawerPro v-model="open" :header="$t('ssl.' + operate)" size="large" @close="handleClose">
<el-form ref="sslForm" label-position="top" :model="ssl" label-width="100px" :rules="rules" v-loading="loading">
<el-row :gutter="20">
<el-form
ref="sslForm"
label-position="top"
:model="ssl"
label-width="100px"
:rules="rules"
:disabled="loading"
v-loading="loading"
>
<el-form-item :label="$t('commons.table.type')" v-if="operate === 'create'">
<el-radio-group v-model="certificateType" @change="changeCertificateType">
<el-radio-button value="acme">ACME</el-radio-button>
<el-radio-button value="selfSigned">{{ $t('ssl.selfSigned') }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="ssl.provider === 'selfSigned' && operate === 'create'"
:label="$t('ssl.ca')"
prop="caId"
>
<el-select v-model="ssl.caId" filterable :loading="caLoading">
<el-option v-for="ca in cas" :key="ca.id" :label="ca.name" :value="ca.id" />
</el-select>
</el-form-item>
<el-form-item v-if="ssl.provider === 'selfSigned'" :label="$t('website.domain')" prop="domains">
<el-input v-model="ssl.domains" type="textarea" :rows="4" :placeholder="$t('ssl.domainHelper')" />
</el-form-item>
<el-row v-else :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('website.primaryDomain')" prop="primaryDomain">
<el-input v-model.trim="ssl.primaryDomain"></el-input>
@@ -20,10 +46,14 @@
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="''" prop="autoRenew">
<el-form-item :label="''" prop="isIP" v-if="ssl.provider !== 'selfSigned'">
<el-checkbox v-model="ssl.isIP" :label="$t('ssl.isIP')" @change="changeIP" />
</el-form-item>
<el-form-item :label="$t('website.otherDomains')" prop="otherDomains" v-if="!ssl.isIP">
<el-form-item
:label="$t('website.otherDomains')"
prop="otherDomains"
v-if="ssl.provider !== 'selfSigned' && !ssl.isIP"
>
<el-input type="textarea" :rows="3" v-model="ssl.otherDomains"></el-input>
</el-form-item>
<el-form-item :label="$t('website.remark')" prop="description">
@@ -61,6 +91,20 @@
></el-option>
</el-select>
</el-form-item>
<el-form-item
v-if="ssl.provider === 'selfSigned' && operate === 'create'"
:label="$t('ssl.days')"
prop="time"
>
<el-input type="number" v-model.number="ssl.time">
<template #append>
<el-select v-model="ssl.unit" style="width: 100px">
<el-option :label="$t('commons.units.day')" value="day" />
<el-option :label="$t('commons.units.year')" value="year" />
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('website.provider')" prop="provider" v-if="ssl.provider != 'selfSigned'">
<el-radio-group v-model="ssl.provider" @change="changeProvider()">
<el-radio value="dnsAccount" :disabled="ssl.isIP">{{ $t('website.dnsAccount') }}</el-radio>
@@ -123,6 +167,8 @@
{{ $t('ssl.nameserverHelper') }}
</span>
</el-form-item>
</div>
<div v-if="ssl.provider !== 'selfSigned' || operate === 'create'">
<el-form-item :label="''" prop="pushDir">
<el-checkbox v-model="ssl.pushDir" :label="$t('ssl.pushDir')" />
</el-form-item>
@@ -184,12 +230,20 @@
<script lang="ts" setup>
import { Website } from '@/api/interface/website';
import { createSSL, listWebsites, searchAcmeAccount, searchDnsAccount, updateSSL } from '@/api/modules/website';
import { Rules, checkMaxLength } from '@/global/form-rules';
import {
createSSL,
listWebsites,
searchAcmeAccount,
searchDnsAccount,
updateSSL,
searchCAs,
obtainSSLByCA,
} from '@/api/modules/website';
import { Rules, checkMaxLength, checkNumberRange } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { computed, reactive, ref, watch } from 'vue';
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { KeyTypes } from '@/global/mimetype';
import { getDNSName, getAccountName } from '@/utils/ssl';
@@ -232,7 +286,14 @@ const dnsAccounts = ref<Website.DnsAccount[]>();
const acmeAccounts = ref<Website.AcmeAccount[]>();
const sslForm = ref<FormInstance>();
const websites = ref();
const rules = ref({
const certificateType = ref('acme');
const cas = ref<Website.CA[]>([]);
const caLoading = ref(false);
let caRequestVersion = 0;
const rules = computed(() => ({
caId: [Rules.requiredSelectBusiness],
time: [Rules.requiredInput, Rules.integerNumber, checkNumberRange(1, 10000)],
domains: [Rules.requiredInput],
primaryDomain: [Rules.requiredInput, Rules.domainOrIP],
acmeAccountId: [Rules.requiredSelectBusiness],
dnsAccountId: [Rules.requiredSelectBusiness],
@@ -245,10 +306,14 @@ const rules = ref({
shell: [Rules.requiredInput],
description: [checkMaxLength(128)],
pushNodes: [Rules.requiredSelect],
});
}));
const websiteID = ref();
const initData = () => ({
domains: '',
caId: undefined as number | undefined,
time: 10,
unit: 'year',
id: 0,
primaryDomain: '',
otherDomains: '',
@@ -297,6 +362,10 @@ const resetForm = () => {
dnsResolve.value = [];
ssl.value = initData();
websiteID.value = undefined;
certificateType.value = 'acme';
cas.value = [];
caRequestVersion++;
caLoading.value = false;
};
const changeIP = () => {
@@ -331,6 +400,9 @@ const acceptParams = (op: string, websiteSSL?: Website.SSLDTO) => {
ssl.value.description = websiteSSL.description;
ssl.value.id = websiteSSL.id;
ssl.value.provider = websiteSSL.provider;
ssl.value.domains = [websiteSSL.primaryDomain, ...(websiteSSL.domains || '').split(',')]
.filter(Boolean)
.join('\n');
ssl.value.skipDNS = websiteSSL.skipDNS;
ssl.value.disableCNAME = websiteSSL.disableCNAME;
ssl.value.nameserver1 = websiteSSL.nameserver1;
@@ -338,9 +410,6 @@ const acceptParams = (op: string, websiteSSL?: Website.SSLDTO) => {
ssl.value.keyType = websiteSSL.keyType;
ssl.value.execShell = websiteSSL.execShell;
ssl.value.shell = websiteSSL.shell;
if (ssl.value.provider == 'selfSigned') {
rules.value.primaryDomain = [];
}
ssl.value.pushNode = websiteSSL.pushNode;
if (websiteSSL.nodes != '') {
ssl.value.pushNodes = websiteSSL.nodes
@@ -357,6 +426,33 @@ const acceptParams = (op: string, websiteSSL?: Website.SSLDTO) => {
open.value = true;
};
const changeCertificateType = async () => {
ssl.value.provider = certificateType.value === 'selfSigned' ? 'selfSigned' : 'dnsAccount';
ssl.value.isIP = false;
await nextTick();
sslForm.value?.clearValidate();
if (certificateType.value !== 'selfSigned' || cas.value.length || caLoading.value) return;
caLoading.value = true;
const requestVersion = ++caRequestVersion;
try {
const items: Website.CA[] = [];
let page = 1;
while (true) {
const res = await searchCAs({ page, pageSize: 100 });
if (requestVersion !== caRequestVersion) return;
const batch = res.data.items || [];
items.push(...batch);
if (!batch.length || items.length >= res.data.total) break;
page++;
}
if (!open.value) return;
cas.value = items;
ssl.value.caId = items[0]?.id;
} finally {
if (requestVersion === caRequestVersion) caLoading.value = false;
}
};
const getPath = (dirs: string[]) => {
ssl.value.dir = dirs.join('\n');
};
@@ -404,9 +500,9 @@ const changeWebsite = () => {
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
if (!formEl || loading.value) return;
await formEl.validate((valid) => {
if (!valid) {
if (!valid || loading.value) {
return;
}
let nodes = '';
@@ -416,6 +512,35 @@ const submit = async (formEl: FormInstance | undefined) => {
loading.value = true;
if (operate.value == 'create') {
ssl.value.nodes = nodes;
if (ssl.value.provider === 'selfSigned') {
obtainSSLByCA({
id: ssl.value.caId!,
domains: ssl.value.domains
.split('\n')
.map((domain) => domain.trim())
.filter(Boolean)
.join('\n'),
keyType: ssl.value.keyType,
time: ssl.value.time,
unit: ssl.value.unit,
autoRenew: ssl.value.autoRenew,
description: ssl.value.description,
pushDir: ssl.value.pushDir,
dir: ssl.value.dir,
execShell: ssl.value.execShell,
shell: ssl.value.shell,
pushNode: ssl.value.pushNode,
nodes,
})
.then(() => {
handleClose();
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
})
.finally(() => {
loading.value = false;
});
return;
}
createSSL(ssl.value)
.then((res: any) => {
if (ssl.value.provider != 'dnsManual') {
@@ -429,6 +554,14 @@ const submit = async (formEl: FormInstance | undefined) => {
});
}
if (operate.value == 'edit') {
if (ssl.value.provider === 'selfSigned') {
const domains = ssl.value.domains
.split('\n')
.map((domain) => domain.trim())
.filter(Boolean);
ssl.value.primaryDomain = domains[0];
ssl.value.otherDomains = domains.slice(1).join('\n');
}
const sslUpdate = {
id: ssl.value.id,
primaryDomain: ssl.value.primaryDomain,
@@ -1,218 +1,201 @@
<template>
<DialogPro v-model="open" :title="$t('commons.button.' + accountData.mode)" @close="handleClose">
<el-row>
<el-col :span="22" :offset="1">
<el-form ref="accountForm" label-position="top" :model="account" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="account.name"></el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.type')" prop="type">
<el-select v-model="account.type" :disabled="accountData.mode === 'edit'">
<el-option
v-for="(type, index) in DNSTypes"
:key="index"
:label="type.deprecated ? `${type.label} (${$t('ssl.dnsPodRemoved')})` : type.label"
:value="type.value"
:disabled="type.deprecated && accountData.mode === 'create'"
></el-option>
</el-select>
<span class="input-help text-red-500" v-if="account.type === 'DnsPod'">
{{ $t('ssl.dnsPodRemovedTip') }}
</span>
</el-form-item>
<div
v-if="
account.type === 'AliYun' ||
account.type === 'AliESA' ||
account.type === 'HuaweiCloud' ||
account.type === 'BaiduCloud' ||
account.type === 'AWSRoute53'
"
>
<el-form-item label="Access key" prop="authorization.accessKey">
<el-input v-model.trim="account.authorization['accessKey']"></el-input>
</el-form-item>
<el-form-item label="Secret key" prop="authorization.secretKey">
<el-input v-model.trim="account.authorization['secretKey']"></el-input>
</el-form-item>
</div>
<el-form-item label="Region" prop="authorization.region" v-if="account.type === 'HuaweiCloud'">
<el-input v-model.trim="account.authorization['region']" :placeholder="'cn-north-1'"></el-input>
</el-form-item>
<div v-if="account.type === 'TencentCloud'">
<el-form-item label="Secret ID" prop="authorization.secretID">
<el-input v-model.trim="account.authorization['secretID']"></el-input>
</el-form-item>
<el-form-item label="Secret Key" prop="authorization.secretKey">
<el-input v-model.trim="account.authorization['secretKey']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Volcengine'">
<el-form-item label="Access key" prop="authorization.accessKey">
<el-input v-model.trim="account.authorization['accessKey']"></el-input>
</el-form-item>
<el-form-item label="Secret key" prop="authorization.secretKey">
<el-input v-model.trim="account.authorization['secretKey']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'CloudDns'">
<el-form-item label="Client ID" prop="authorization.clientID">
<el-input v-model.trim="account.authorization['clientID']"></el-input>
</el-form-item>
<el-form-item label="Email" prop="authorization.email">
<el-input v-model.trim="account.authorization['email']"></el-input>
</el-form-item>
<el-form-item label="Password" prop="authorization.password">
<el-input v-model.trim="account.authorization['password']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'CloudFlare'">
<el-form-item label="EMAIL" prop="authorization.email">
<el-input v-model.trim="account.authorization['email']"></el-input>
</el-form-item>
<el-form-item label="API Token" prop="authorization.apiKey">
<el-input v-model.trim="account.authorization['apiKey']"></el-input>
<span class="input-help text-red-500">
{{ $t('ssl.cfHelper') }}
</span>
</el-form-item>
</div>
<el-form-item
label="API Key"
prop="authorization.apiKey"
v-if="
account.type === 'NameCheap' ||
account.type === 'NameSilo' ||
account.type === 'Godaddy' ||
account.type === 'RainYun' ||
account.type === 'Spaceship' ||
account.type === 'Dynadot' ||
account.type === 'Dynu'
"
>
<el-input v-model.trim="account.authorization['apiKey']"></el-input>
</el-form-item>
<el-form-item label="API User" prop="authorization.apiUser" v-if="account.type === 'NameCheap'">
<el-input v-model.trim="account.authorization['apiUser']"></el-input>
</el-form-item>
<el-form-item
label="API Secret"
prop="authorization.apiSecret"
v-if="account.type === 'Godaddy' || account.type === 'Spaceship' || account.type === 'Dynadot'"
>
<el-input v-model.trim="account.authorization['apiSecret']"></el-input>
</el-form-item>
<div v-if="account.type === 'NameCom'">
<el-form-item label="Username" prop="authorization.apiUser">
<el-input v-model.trim="account.authorization['apiUser']"></el-input>
</el-form-item>
<el-form-item label="Token" prop="authorization.token">
<el-input v-model.trim="account.authorization['token']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Netcup'">
<el-form-item label="Customer Number" prop="authorization.customerNumber">
<el-input v-model.trim="account.authorization['customerNumber']"></el-input>
</el-form-item>
<el-form-item label="API Key" prop="authorization.apiKey">
<el-input v-model.trim="account.authorization['apiKey']"></el-input>
</el-form-item>
<el-form-item label="API Password" prop="authorization.password">
<el-input
v-model.trim="account.authorization['password']"
type="password"
show-password
></el-input>
</el-form-item>
</div>
<div
v-if="account.type === 'FreeMyIP' || account.type === 'Vercel' || account.type === 'IonosCloud'"
>
<el-form-item label="Token" prop="authorization.token">
<el-input v-model.trim="account.authorization['token']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Ionos'">
<el-form-item label="API Prefix" prop="authorization.apiPrefix">
<el-input v-model.trim="account.authorization['apiPrefix']"></el-input>
</el-form-item>
<el-form-item label="API Secret" prop="authorization.apiSecret">
<el-input v-model.trim="account.authorization['apiSecret']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'ClouDNS'">
<el-form-item label="Auth ID" prop="authorization.authID">
<el-input v-model.trim="account.authorization['authID']"></el-input>
</el-form-item>
<el-form-item label="Sub Auth ID" prop="authorization.subAuthID">
<el-input v-model.trim="account.authorization['subAuthID']"></el-input>
</el-form-item>
<el-form-item label="Auth Password" prop="authorization.authPassword">
<el-input v-model.trim="account.authorization['authPassword']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'WestCN' || account.type === 'RegRu'">
<el-form-item label="Username" prop="authorization.username">
<el-input v-model.trim="account.authorization['username']"></el-input>
</el-form-item>
<el-form-item label="Password" prop="authorization.password">
<el-input v-model.trim="account.authorization['password']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Ovh'">
<el-form-item label="Endpoint" prop="authorization.endpoint" :rules="[Rules.requiredInput]">
<el-input v-model.trim="account.authorization['endpoint']"></el-input>
</el-form-item>
<el-form-item
label="AccessToken"
prop="authorization.accessToken"
:rules="[Rules.requiredInput]"
>
<el-input v-model.trim="account.authorization['accessToken']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'AcmeDNS'">
<el-form-item label="API BASE" prop="authorization.endpoint" :rules="[Rules.requiredInput]">
<el-input v-model.trim="account.authorization['endpoint']"></el-input>
</el-form-item>
<el-form-item label="BASE URL" prop="authorization.baseURL" :rules="[Rules.requiredInput]">
<el-input v-model.trim="account.authorization['baseURL']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'AWSRoute53'">
<el-form-item label="Region" prop="authorization.region">
<el-input
v-model.trim="account.authorization['region']"
:placeholder="'us-east-1'"
></el-input>
</el-form-item>
<el-form-item
:label="$t('ssl.hostedZoneID')"
prop="authorization.endpoint"
:rules="[Rules.requiredInput]"
>
<el-input v-model.trim="account.authorization['endpoint']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'PorkBun'">
<el-form-item label="API Key" prop="authorization.apiKey">
<el-input v-model.trim="account.authorization['apiKey']"></el-input>
</el-form-item>
<el-form-item label="Secret Key" prop="authorization.secretKey">
<el-input v-model.trim="account.authorization['secretKey']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Technitium'">
<el-form-item label="BASE URL" prop="authorization.baseURL" :rules="[Rules.requiredInput]">
<el-input v-model.trim="account.authorization['baseURL']"></el-input>
</el-form-item>
<el-form-item label="Token" prop="authorization.token">
<el-input v-model.trim="account.authorization['token']"></el-input>
</el-form-item>
</div>
</el-form>
</el-col>
</el-row>
<DrawerPro v-model="open" :header="$t('commons.button.' + accountData.mode)" size="large" @close="handleClose">
<el-form v-loading="loading" ref="accountForm" label-position="top" :model="account" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="account.name"></el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.type')" prop="type">
<el-select v-model="account.type" :disabled="accountData.mode === 'edit'">
<el-option
v-for="(type, index) in DNSTypes"
:key="index"
:label="type.deprecated ? `${type.label} (${$t('ssl.dnsPodRemoved')})` : type.label"
:value="type.value"
:disabled="type.deprecated && accountData.mode === 'create'"
></el-option>
</el-select>
<span class="input-help text-red-500" v-if="account.type === 'DnsPod'">
{{ $t('ssl.dnsPodRemovedTip') }}
</span>
</el-form-item>
<div
v-if="
account.type === 'AliYun' ||
account.type === 'AliESA' ||
account.type === 'HuaweiCloud' ||
account.type === 'BaiduCloud' ||
account.type === 'AWSRoute53'
"
>
<el-form-item label="Access key" prop="authorization.accessKey">
<el-input v-model.trim="account.authorization['accessKey']"></el-input>
</el-form-item>
<el-form-item label="Secret key" prop="authorization.secretKey">
<el-input v-model.trim="account.authorization['secretKey']"></el-input>
</el-form-item>
</div>
<el-form-item label="Region" prop="authorization.region" v-if="account.type === 'HuaweiCloud'">
<el-input v-model.trim="account.authorization['region']" :placeholder="'cn-north-1'"></el-input>
</el-form-item>
<div v-if="account.type === 'TencentCloud'">
<el-form-item label="Secret ID" prop="authorization.secretID">
<el-input v-model.trim="account.authorization['secretID']"></el-input>
</el-form-item>
<el-form-item label="Secret Key" prop="authorization.secretKey">
<el-input v-model.trim="account.authorization['secretKey']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Volcengine'">
<el-form-item label="Access key" prop="authorization.accessKey">
<el-input v-model.trim="account.authorization['accessKey']"></el-input>
</el-form-item>
<el-form-item label="Secret key" prop="authorization.secretKey">
<el-input v-model.trim="account.authorization['secretKey']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'CloudDns'">
<el-form-item label="Client ID" prop="authorization.clientID">
<el-input v-model.trim="account.authorization['clientID']"></el-input>
</el-form-item>
<el-form-item label="Email" prop="authorization.email">
<el-input v-model.trim="account.authorization['email']"></el-input>
</el-form-item>
<el-form-item label="Password" prop="authorization.password">
<el-input v-model.trim="account.authorization['password']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'CloudFlare'">
<el-form-item label="EMAIL" prop="authorization.email">
<el-input v-model.trim="account.authorization['email']"></el-input>
</el-form-item>
<el-form-item label="API Token" prop="authorization.apiKey">
<el-input v-model.trim="account.authorization['apiKey']"></el-input>
<span class="input-help text-red-500">
{{ $t('ssl.cfHelper') }}
</span>
</el-form-item>
</div>
<el-form-item
label="API Key"
prop="authorization.apiKey"
v-if="
account.type === 'NameCheap' ||
account.type === 'NameSilo' ||
account.type === 'Godaddy' ||
account.type === 'RainYun' ||
account.type === 'Spaceship' ||
account.type === 'Dynadot' ||
account.type === 'Dynu'
"
>
<el-input v-model.trim="account.authorization['apiKey']"></el-input>
</el-form-item>
<el-form-item label="API User" prop="authorization.apiUser" v-if="account.type === 'NameCheap'">
<el-input v-model.trim="account.authorization['apiUser']"></el-input>
</el-form-item>
<el-form-item
label="API Secret"
prop="authorization.apiSecret"
v-if="account.type === 'Godaddy' || account.type === 'Spaceship' || account.type === 'Dynadot'"
>
<el-input v-model.trim="account.authorization['apiSecret']"></el-input>
</el-form-item>
<div v-if="account.type === 'NameCom'">
<el-form-item label="Username" prop="authorization.apiUser">
<el-input v-model.trim="account.authorization['apiUser']"></el-input>
</el-form-item>
<el-form-item label="Token" prop="authorization.token">
<el-input v-model.trim="account.authorization['token']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Netcup'">
<el-form-item label="Customer Number" prop="authorization.customerNumber">
<el-input v-model.trim="account.authorization['customerNumber']"></el-input>
</el-form-item>
<el-form-item label="API Key" prop="authorization.apiKey">
<el-input v-model.trim="account.authorization['apiKey']"></el-input>
</el-form-item>
<el-form-item label="API Password" prop="authorization.password">
<el-input v-model.trim="account.authorization['password']" type="password" show-password></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'FreeMyIP' || account.type === 'Vercel' || account.type === 'IonosCloud'">
<el-form-item label="Token" prop="authorization.token">
<el-input v-model.trim="account.authorization['token']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Ionos'">
<el-form-item label="API Prefix" prop="authorization.apiPrefix">
<el-input v-model.trim="account.authorization['apiPrefix']"></el-input>
</el-form-item>
<el-form-item label="API Secret" prop="authorization.apiSecret">
<el-input v-model.trim="account.authorization['apiSecret']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'ClouDNS'">
<el-form-item label="Auth ID" prop="authorization.authID">
<el-input v-model.trim="account.authorization['authID']"></el-input>
</el-form-item>
<el-form-item label="Sub Auth ID" prop="authorization.subAuthID">
<el-input v-model.trim="account.authorization['subAuthID']"></el-input>
</el-form-item>
<el-form-item label="Auth Password" prop="authorization.authPassword">
<el-input v-model.trim="account.authorization['authPassword']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'WestCN' || account.type === 'RegRu'">
<el-form-item label="Username" prop="authorization.username">
<el-input v-model.trim="account.authorization['username']"></el-input>
</el-form-item>
<el-form-item label="Password" prop="authorization.password">
<el-input v-model.trim="account.authorization['password']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Ovh'">
<el-form-item label="Endpoint" prop="authorization.endpoint" :rules="[Rules.requiredInput]">
<el-input v-model.trim="account.authorization['endpoint']"></el-input>
</el-form-item>
<el-form-item label="AccessToken" prop="authorization.accessToken" :rules="[Rules.requiredInput]">
<el-input v-model.trim="account.authorization['accessToken']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'AcmeDNS'">
<el-form-item label="API BASE" prop="authorization.endpoint" :rules="[Rules.requiredInput]">
<el-input v-model.trim="account.authorization['endpoint']"></el-input>
</el-form-item>
<el-form-item label="BASE URL" prop="authorization.baseURL" :rules="[Rules.requiredInput]">
<el-input v-model.trim="account.authorization['baseURL']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'AWSRoute53'">
<el-form-item label="Region" prop="authorization.region">
<el-input v-model.trim="account.authorization['region']" :placeholder="'us-east-1'"></el-input>
</el-form-item>
<el-form-item
:label="$t('ssl.hostedZoneID')"
prop="authorization.endpoint"
:rules="[Rules.requiredInput]"
>
<el-input v-model.trim="account.authorization['endpoint']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'PorkBun'">
<el-form-item label="API Key" prop="authorization.apiKey">
<el-input v-model.trim="account.authorization['apiKey']"></el-input>
</el-form-item>
<el-form-item label="Secret Key" prop="authorization.secretKey">
<el-input v-model.trim="account.authorization['secretKey']"></el-input>
</el-form-item>
</div>
<div v-if="account.type === 'Technitium'">
<el-form-item label="BASE URL" prop="authorization.baseURL" :rules="[Rules.requiredInput]">
<el-input v-model.trim="account.authorization['baseURL']"></el-input>
</el-form-item>
<el-form-item label="Token" prop="authorization.token">
<el-input v-model.trim="account.authorization['token']"></el-input>
</el-form-item>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
@@ -221,7 +204,7 @@
</el-button>
</span>
</template>
</DialogPro>
</DrawerPro>
</template>
<script lang="ts" setup>
@@ -269,60 +252,44 @@ const account = ref({
type: 'AliYun',
authorization: {},
});
const em = defineEmits(['close']);
const em = defineEmits(['saved']);
const handleClose = () => {
resetForm();
open.value = false;
em('close', false);
};
const resetForm = () => {
account.value = {
id: 0,
name: '',
type: 'AliYun',
authorization: {},
};
accountForm.value?.resetFields();
account.value = { id: 0, name: '', type: 'AliYun', authorization: {} };
};
const acceptParams = async (props: AccountProps) => {
const acceptParams = (props: AccountProps) => {
resetForm();
accountData.value.mode = props.mode;
if (props.mode === 'edit') {
account.value = props.form;
account.value = { ...props.form, authorization: { ...props.form.authorization } };
}
open.value = true;
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
if (accountData.value.mode === 'edit') {
updateDnsAccount(account.value)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
} else {
createDnsAccount(account.value)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
}
});
if (!formEl || loading.value) return;
loading.value = true;
try {
if (!(await formEl.validate().catch(() => false))) return;
const save = accountData.value.mode === 'edit' ? updateDnsAccount : createDnsAccount;
await save(account.value);
MsgSuccess(
i18n.global.t(
accountData.value.mode === 'edit' ? 'commons.msg.updateSuccess' : 'commons.msg.createSuccess',
),
);
handleClose();
em('saved');
} finally {
loading.value = false;
}
};
defineExpose({
@@ -1,40 +1,47 @@
<template>
<DrawerPro v-model="open" :header="$t('website.dnsAccountManage')" size="large" @close="handleClose">
<template #content>
<ComplexTable :data="data" :pagination-config="paginationConfig" @search="search()">
<template #toolbar>
<el-button v-permission type="primary" @click="openCreate">
{{ $t('commons.button.create') }}
</el-button>
</template>
<el-table-column
:label="$t('commons.table.name')"
fix
show-overflow-tooltip
prop="name"
></el-table-column>
<el-table-column :label="$t('commons.table.type')" prop="type">
<template #default="{ row }">
<span>{{ getDNSName(row.type) }}</span>
<el-tooltip v-if="row.type === 'DnsPod'" :content="$t('ssl.dnsPodRemovedTip')" placement="top">
<el-tag type="danger" size="small" class="ml-2">
{{ $t('ssl.dnsPodRemoved') }}
</el-tag>
</el-tooltip>
</template>
</el-table-column>
<fu-table-operations
:ellipsis="1"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
<Create ref="createRef" @close="search()"></Create>
</template>
</DrawerPro>
<OpDialog ref="opRef" @search="search" />
<div>
<RouterButton :buttons="routerButton" />
<LayoutContent backName="SSL" :title="$t('website.dnsAccountManage')">
<template #leftToolBar>
<el-button v-permission type="primary" @click="openCreate">
{{ $t('commons.button.create') }}
</el-button>
</template>
<template #main>
<ComplexTable :data="data" :pagination-config="paginationConfig" @search="search()">
<el-table-column
:label="$t('commons.table.name')"
fix
show-overflow-tooltip
prop="name"
></el-table-column>
<el-table-column :label="$t('commons.table.type')" prop="type">
<template #default="{ row }">
<span>{{ getDNSName(row.type) }}</span>
<el-tooltip
v-if="row.type === 'DnsPod'"
:content="$t('ssl.dnsPodRemovedTip')"
placement="top"
>
<el-tag type="danger" size="small" class="ml-2">
{{ $t('ssl.dnsPodRemoved') }}
</el-tag>
</el-tooltip>
</template>
</el-table-column>
<fu-table-operations
:ellipsis="1"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<Create ref="createRef" @saved="search()" />
<OpDialog drawer ref="opRef" @search="search" />
</div>
</template>
<script lang="ts" setup>
@@ -42,8 +49,9 @@ import { readCachedPageSize } from '@/utils/panel-cache';
import Create from './create/index.vue';
import { Website } from '@/api/interface/website';
import { deleteDnsAccount, searchDnsAccount } from '@/api/modules/website';
import { onMounted, reactive, ref } from 'vue';
import { onMounted, reactive, ref, toRef } from 'vue';
import i18n from '@/lang';
import { useSearchReset } from '@/composables/useSearchReset';
import { getDNSName } from '@/utils/ssl';
const paginationConfig = reactive({
cacheSizeKey: 'dns-account-page-size',
@@ -53,7 +61,6 @@ const paginationConfig = reactive({
});
let data = ref<Website.DnsAccount[]>();
let createRef = ref();
let open = ref(false);
const opRef = ref();
const buttons = [
@@ -73,15 +80,6 @@ const buttons = [
},
];
const acceptParams = () => {
search();
open.value = true;
};
const handleClose = () => {
open.value = false;
};
const search = () => {
const req = {
page: paginationConfig.currentPage,
@@ -118,7 +116,6 @@ onMounted(() => {
search();
});
defineExpose({
acceptParams,
});
const routerButton = [{ label: i18n.global.t('website.dnsAccountManage'), path: '/websites/ssl/dns-account' }];
useSearchReset({ page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
</script>
+4 -12
View File
@@ -172,15 +172,12 @@
/>
</ComplexTable>
</template>
<DnsAccount ref="dnsAccountRef" />
<AcmeAccount ref="acmeAccountRef" />
<Create ref="sslCreateRef" @close="search()" @submit="openLog" />
<Detail ref="detailRef" />
<SSLUpload ref="sslUploadRef" @close="search()" />
<Apply ref="applyRef" @search="search" @submit="openLog" />
<OpDialog ref="opRef" @search="search" @cancel="search" />
<Log ref="logRef" @close="search()" :heightDiff="220" />
<CA ref="caRef" @close="search()" />
<Obtain ref="obtainRef" @close="search()" @submit="openLog" />
<DrawerPro v-model="pushOpen" :header="$t('commons.button.sync')" size="large" @close="handlePushClose">
<el-form
@@ -224,9 +221,7 @@
import { useSearchState } from '@/utils/search-reset';
import { toRef, defineAsyncComponent, onMounted, reactive, ref } from 'vue';
import { deleteSSL, downloadFile, pushSSLToNode, searchSSL, updateSSL } from '@/api/modules/website';
import DnsAccount from './dns-account/index.vue';
import AcmeAccount from './acme-account/index.vue';
import CA from './ca/index.vue';
import { routerToNameWithQuery } from '@/utils/router';
import Create from './create/index.vue';
import Detail from './detail/index.vue';
import { dateFormat } from '@/utils/date';
@@ -263,8 +258,6 @@ const paginationConfig = reactive({
pageSize: 20,
total: 0,
});
const acmeAccountRef = ref();
const dnsAccountRef = ref();
const sslCreateRef = ref();
const detailRef = ref();
const data = ref();
@@ -274,7 +267,6 @@ const sslUploadRef = ref();
const applyRef = ref();
const logRef = ref();
const taskLogRef = ref();
const caRef = ref();
const obtainRef = ref();
const pushFormRef = ref<FormInstance>();
const pushOpen = ref(false);
@@ -464,10 +456,10 @@ const updateConfig = (row: Website.SSLDTO) => {
};
const openAcmeAccount = () => {
acmeAccountRef.value.acceptParams();
routerToNameWithQuery('SSL-ACME', { uncached: 'true' });
};
const openDnsAccount = () => {
dnsAccountRef.value.acceptParams();
routerToNameWithQuery('SSL-DNS', { uncached: 'true' });
};
const openSSL = () => {
sslCreateRef.value.acceptParams('create');
@@ -542,7 +534,7 @@ const submitPush = async () => {
};
const openCA = () => {
caRef.value.acceptParams();
routerToNameWithQuery('SSL-CA', { uncached: 'true' });
};
const applySSL = (row: Website.SSLDTO) => {