feat: 增加自签证书功能 (#3049)

Refs https://github.com/1Panel-dev/1Panel/issues/1475
pull/3070/head
zhengkunwang 2023-11-27 12:04:09 +08:00 committed by GitHub
parent b5592327dd
commit bb56231055
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 2043 additions and 73 deletions

View File

@ -59,4 +59,6 @@ var (
recycleBinService = service.NewIRecycleBinService()
favoriteService = service.NewIFavoriteService()
websiteCAService = service.NewIWebsiteCAService()
)

View File

@ -0,0 +1,118 @@
package v1
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
)
// @Tags Website CA
// @Summary Page website ca
// @Description 获取网站 ca 列表分页
// @Accept json
// @Param request body request.WebsiteCASearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /websites/ca/search [post]
func (b *BaseApi) PageWebsiteCA(c *gin.Context) {
var req request.WebsiteCASearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, cas, err := websiteCAService.Page(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Total: total,
Items: cas,
})
}
// @Tags Website CA
// @Summary Create website ca
// @Description 创建网站 ca
// @Accept json
// @Param request body request.WebsiteCACreate true "request"
// @Success 200 {object} request.WebsiteCACreate
// @Security ApiKeyAuth
// @Router /websites/ca [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 ca [name]","formatEN":"Create website ca [name]"}
func (b *BaseApi) CreateWebsiteCA(c *gin.Context) {
var req request.WebsiteCACreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := websiteCAService.Create(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags Website CA
// @Summary Get website ca
// @Description 获取网站 ca
// @Accept json
// @Param id path int true "id"
// @Success 200 {object} response.WebsiteCADTO
// @Security ApiKeyAuth
// @Router /websites/ca/{id} [get]
func (b *BaseApi) GetWebsiteCA(c *gin.Context) {
id, err := helper.GetParamID(c)
if err != nil {
return
}
res, err := websiteCAService.GetCA(id)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags Website CA
// @Summary Delete website ca
// @Description 删除网站 ca
// @Accept json
// @Param request body request.WebsiteCommonReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/ca/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"删除网站 ca [name]","formatEN":"Delete website ca [name]"}
func (b *BaseApi) DeleteWebsiteCA(c *gin.Context) {
var req request.WebsiteCommonReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteCAService.Delete(req.ID); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags Website CA
// @Summary Obtain SSL
// @Description 自签 SSL 证书
// @Accept json
// @Param request body request.WebsiteCAObtain true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/ca/obtain [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"自签 SSL 证书 [name]","formatEN":"Obtain SSL [name]"}
func (b *BaseApi) ObtainWebsiteCA(c *gin.Context) {
var req request.WebsiteCAObtain
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteCAService.ObtainSSL(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}

View File

@ -68,3 +68,27 @@ type WebsiteSSLUpload struct {
CertificatePath string `json:"certificatePath"`
Type string `json:"type" validate:"required,oneof=paste local"`
}
type WebsiteCASearch struct {
dto.PageInfo
}
type WebsiteCACreate struct {
CommonName string `json:"commonName" validate:"required"`
Country string `json:"country" validate:"required"`
Email string `json:"email" validate:"required"`
Organization string `json:"organization" validate:"required"`
OrganizationUint string `json:"organizationUint"`
Name string `json:"name" validate:"required"`
KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"`
Province string `json:"province" `
City string `json:"city"`
}
type WebsiteCAObtain struct {
ID uint `json:"id" validate:"required"`
Domains string `json:"domains" validate:"required"`
KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"`
Time int `json:"time" validate:"required"`
Unit string `json:"unit" validate:"required"`
}

View File

@ -22,3 +22,7 @@ type WebsiteDnsAccountDTO struct {
model.WebsiteDnsAccount
Authorization map[string]string `json:"authorization"`
}
type WebsiteCADTO struct {
model.WebsiteCA
}

View File

@ -0,0 +1,9 @@
package model
type WebsiteCA struct {
BaseModel
CSR string `gorm:"not null;" json:"csr"`
Name string `gorm:"not null;" json:"name"`
PrivateKey string `gorm:"not null" json:"privateKey"`
KeyType string `gorm:"not null;default:2048" json:"keyType"`
}

View File

@ -0,0 +1,54 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/backend/app/model"
)
type WebsiteCARepo struct {
}
func NewIWebsiteCARepo() IWebsiteCARepo {
return &WebsiteCARepo{}
}
type IWebsiteCARepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteCA, error)
GetFirst(opts ...DBOption) (model.WebsiteCA, error)
List(opts ...DBOption) ([]model.WebsiteCA, error)
Create(ctx context.Context, ca *model.WebsiteCA) error
DeleteBy(opts ...DBOption) error
}
func (w WebsiteCARepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteCA, error) {
var caList []model.WebsiteCA
db := getDb(opts...).Model(&model.WebsiteCA{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&caList).Error
return count, caList, err
}
func (w WebsiteCARepo) GetFirst(opts ...DBOption) (model.WebsiteCA, error) {
var ca model.WebsiteCA
db := getDb(opts...).Model(&model.WebsiteCA{})
if err := db.First(&ca).Error; err != nil {
return ca, err
}
return ca, nil
}
func (w WebsiteCARepo) List(opts ...DBOption) ([]model.WebsiteCA, error) {
var caList []model.WebsiteCA
db := getDb(opts...).Model(&model.WebsiteCA{})
err := db.Find(&caList).Error
return caList, err
}
func (w WebsiteCARepo) Create(ctx context.Context, ca *model.WebsiteCA) error {
return getTx(ctx).Create(ca).Error
}
func (w WebsiteCARepo) DeleteBy(opts ...DBOption) error {
return getDb(opts...).Delete(&model.WebsiteCA{}).Error
}

View File

@ -32,6 +32,7 @@ var (
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
websiteSSLRepo = repo.NewISSLRepo()
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
websiteCARepo = repo.NewIWebsiteCARepo()
logRepo = repo.NewILogRepo()
snapshotRepo = repo.NewISnapshotRepo()

View File

@ -0,0 +1,311 @@
package service
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/ssl"
"github.com/go-acme/lego/v4/certcrypto"
"math/big"
"net"
"strings"
"time"
)
type WebsiteCAService struct {
}
type IWebsiteCAService interface {
Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error)
Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error)
GetCA(id uint) (response.WebsiteCADTO, error)
Delete(id uint) error
ObtainSSL(req request.WebsiteCAObtain) error
}
func NewIWebsiteCAService() IWebsiteCAService {
return &WebsiteCAService{}
}
func (w WebsiteCAService) Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) {
total, cas, err := websiteCARepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc"))
if err != nil {
return 0, nil, err
}
var caDTOs []response.WebsiteCADTO
for _, ca := range cas {
caDTOs = append(caDTOs, response.WebsiteCADTO{
WebsiteCA: ca,
})
}
return total, caDTOs, err
}
func (w WebsiteCAService) Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) {
if exist, _ := websiteCARepo.GetFirst(commonRepo.WithByName(create.Name)); exist.ID > 0 {
return nil, buserr.New(constant.ErrNameIsExist)
}
ca := &model.WebsiteCA{
Name: create.Name,
KeyType: create.KeyType,
}
pkixName := pkix.Name{
CommonName: create.CommonName,
Country: []string{create.Country},
Organization: []string{create.Organization},
}
if create.Province != "" {
pkixName.Province = []string{create.Province}
}
if create.City != "" {
pkixName.Locality = []string{create.City}
}
rootCA := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix()),
Subject: pkixName,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
MaxPathLenZero: false,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
}
privateKey, err := certcrypto.GeneratePrivateKey(ssl.KeyType(create.KeyType))
if err != nil {
return nil, err
}
var (
publicKey any
caPEM = new(bytes.Buffer)
caPrivateKeyPEM = new(bytes.Buffer)
privateBlock = &pem.Block{}
)
if ssl.KeyType(create.KeyType) == certcrypto.EC256 || ssl.KeyType(create.KeyType) == certcrypto.EC384 {
publicKey = &privateKey.(*ecdsa.PrivateKey).PublicKey
publicKey = publicKey.(*ecdsa.PublicKey)
privateBlock.Type = "EC PRIVATE KEY"
privateBytes, err := x509.MarshalECPrivateKey(privateKey.(*ecdsa.PrivateKey))
if err != nil {
return nil, err
}
privateBlock.Bytes = privateBytes
_ = pem.Encode(caPrivateKeyPEM, privateBlock)
} else {
publicKey = privateKey.(*rsa.PrivateKey).PublicKey
publicKey = publicKey.(*rsa.PublicKey)
privateBlock.Type = "RSA PRIVATE KEY"
privateBlock.Bytes = x509.MarshalPKCS1PrivateKey(privateKey.(*rsa.PrivateKey))
}
ca.PrivateKey = string(pem.EncodeToMemory(privateBlock))
caBytes, err := x509.CreateCertificate(rand.Reader, rootCA, rootCA, publicKey, privateKey)
if err != nil {
return nil, err
}
certBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
}
_ = pem.Encode(caPEM, certBlock)
pemData := pem.EncodeToMemory(certBlock)
ca.CSR = string(pemData)
if err := websiteCARepo.Create(context.Background(), ca); err != nil {
return nil, err
}
return &create, nil
}
func (w WebsiteCAService) GetCA(id uint) (response.WebsiteCADTO, error) {
ca, err := websiteCARepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return response.WebsiteCADTO{}, err
}
return response.WebsiteCADTO{
WebsiteCA: ca,
}, nil
}
func (w WebsiteCAService) Delete(id uint) error {
return websiteCARepo.DeleteBy(commonRepo.WithByID(id))
}
func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) error {
ca, err := websiteCARepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
newSSL := &model.WebsiteSSL{
Provider: constant.SelfSigned,
KeyType: req.KeyType,
}
var (
domains []string
ips []net.IP
)
if req.Domains != "" {
domainArray := strings.Split(req.Domains, "\n")
for _, domain := range domainArray {
if !common.IsValidDomain(domain) {
err = buserr.WithName("ErrDomainFormat", domain)
return err
} else {
if ipAddress := net.ParseIP(domain); ipAddress == nil {
domains = append(domains, domain)
} else {
ips = append(ips, ipAddress)
}
}
}
if len(domains) > 0 {
newSSL.PrimaryDomain = domains[0]
newSSL.Domains = strings.Join(domains[1:], ",")
}
}
rootCertBlock, _ := pem.Decode([]byte(ca.CSR))
if rootCertBlock == nil {
return buserr.New("ErrSSLCertificateFormat")
}
rootCsr, err := x509.ParseCertificate(rootCertBlock.Bytes)
if err != nil {
return err
}
rootPrivateKeyBlock, _ := pem.Decode([]byte(ca.PrivateKey))
if rootPrivateKeyBlock == nil {
return buserr.New("ErrSSLCertificateFormat")
}
var rootPrivateKey any
if ssl.KeyType(ca.KeyType) == certcrypto.EC256 || ssl.KeyType(ca.KeyType) == certcrypto.EC384 {
rootPrivateKey, err = x509.ParseECPrivateKey(rootPrivateKeyBlock.Bytes)
if err != nil {
return err
}
} else {
rootPrivateKey, err = x509.ParsePKCS1PrivateKey(rootPrivateKeyBlock.Bytes)
if err != nil {
return err
}
}
interPrivateKey, interPublicKey, _, err := createPrivateKey(req.KeyType)
if err != nil {
return err
}
notAfter := time.Now()
if req.Unit == "year" {
notAfter = notAfter.AddDate(req.Time, 0, 0)
} else {
notAfter = notAfter.AddDate(0, 0, req.Time)
}
interCsr := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix()),
Subject: rootCsr.Subject,
NotBefore: time.Now(),
NotAfter: notAfter,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 0,
MaxPathLenZero: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
}
interDer, err := x509.CreateCertificate(rand.Reader, interCsr, rootCsr, interPublicKey, rootPrivateKey)
if err != nil {
return err
}
interCert, err := x509.ParseCertificate(interDer)
if err != nil {
return err
}
_, publicKey, privateKeyBytes, err := createPrivateKey(req.KeyType)
if err != nil {
return err
}
csr := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix()),
Subject: rootCsr.Subject,
NotBefore: time.Now(),
NotAfter: notAfter,
BasicConstraintsValid: true,
IsCA: false,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: domains,
IPAddresses: ips,
}
der, err := x509.CreateCertificate(rand.Reader, csr, interCert, publicKey, interPrivateKey)
if err != nil {
return err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return err
}
certBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}
pemData := pem.EncodeToMemory(certBlock)
newSSL.Pem = string(pemData)
newSSL.PrivateKey = string(privateKeyBytes)
newSSL.ExpireDate = cert.NotAfter
newSSL.StartDate = cert.NotBefore
newSSL.Type = cert.Issuer.CommonName
newSSL.Organization = rootCsr.Subject.Organization[0]
return websiteSSLRepo.Create(context.Background(), newSSL)
}
func createPrivateKey(keyType string) (privateKey any, publicKey any, privateKeyBytes []byte, err error) {
privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(keyType))
if err != nil {
return
}
var (
caPrivateKeyPEM = new(bytes.Buffer)
)
if ssl.KeyType(keyType) == certcrypto.EC256 || ssl.KeyType(keyType) == certcrypto.EC384 {
publicKey = &privateKey.(*ecdsa.PrivateKey).PublicKey
publicKey = publicKey.(*ecdsa.PublicKey)
block := &pem.Block{
Type: "EC PRIVATE KEY",
}
privateBytes, sErr := x509.MarshalECPrivateKey(privateKey.(*ecdsa.PrivateKey))
if sErr != nil {
err = sErr
return
}
block.Bytes = privateBytes
_ = pem.Encode(caPrivateKeyPEM, block)
} else {
publicKey = privateKey.(*rsa.PrivateKey).PublicKey
publicKey = publicKey.(*rsa.PublicKey)
_ = pem.Encode(caPrivateKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey.(*rsa.PrivateKey)),
})
}
privateKeyBytes = caPrivateKeyPEM.Bytes()
return
}

View File

@ -148,7 +148,6 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
}
func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
var (
err error
websiteSSL model.WebsiteSSL
@ -212,7 +211,7 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
}
go func() {
logFile, _ := os.OpenFile(path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
logFile, _ := os.OpenFile(path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
defer logFile.Close()
logger := log.New(logFile, "", log.LstdFlags)
legoLogger.Logger = logger
@ -443,10 +442,17 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
if len(cert.DNSNames) > 0 {
newSSL.PrimaryDomain = cert.DNSNames[0]
domains = cert.DNSNames[1:]
} else if len(cert.IPAddresses) > 0 {
newSSL.PrimaryDomain = cert.IPAddresses[0].String()
for _, ip := range cert.IPAddresses[1:] {
domains = append(domains, ip.String())
}
if len(cert.IPAddresses) > 0 {
if newSSL.PrimaryDomain == "" {
newSSL.PrimaryDomain = cert.IPAddresses[0].String()
for _, ip := range cert.IPAddresses[1:] {
domains = append(domains, ip.String())
}
} else {
for _, ip := range cert.IPAddresses {
domains = append(domains, ip.String())
}
}
}
newSSL.Domains = strings.Join(domains, ",")

View File

@ -27,6 +27,7 @@ const (
DnsManual = "dnsManual"
Http = "http"
Manual = "manual"
SelfSigned = "selfSigned"
StartWeb = "start"
StopWeb = "stop"

View File

@ -55,6 +55,7 @@ func Init() {
migrations.UpdateAcmeAccount,
migrations.UpdateWebsiteSSL,
migrations.AddWebsiteCA,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -25,3 +25,13 @@ var UpdateWebsiteSSL = &gormigrate.Migration{
return nil
},
}
var AddWebsiteCA = &gormigrate.Migration{
ID: "20231125-add-website-ca",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.WebsiteCA{}); err != nil {
return err
}
return nil
},
}

View File

@ -96,6 +96,7 @@ func Routers() *gin.Engine {
systemRouter.InitRuntimeRouter(PrivateGroup)
systemRouter.InitProcessRouter(PrivateGroup)
systemRouter.InitToolboxRouter(PrivateGroup)
systemRouter.InitWebsiteCARouter(PrivateGroup)
}
return Router

View File

@ -0,0 +1,20 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
"github.com/1Panel-dev/1Panel/backend/middleware"
"github.com/gin-gonic/gin"
)
func (a *WebsiteDnsAccountRouter) InitWebsiteCARouter(Router *gin.RouterGroup) {
groupRouter := Router.Group("websites/ca")
groupRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
groupRouter.POST("/search", baseApi.PageWebsiteCA)
groupRouter.POST("", baseApi.CreateWebsiteCA)
groupRouter.POST("/del", baseApi.DeleteWebsiteCA)
groupRouter.POST("/obtain", baseApi.ObtainWebsiteCA)
}
}

View File

@ -1,5 +1,5 @@
// Code generated by swaggo/swag. DO NOT EDIT.
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import "github.com/swaggo/swag"
@ -11016,6 +11016,223 @@ const docTemplate = `{
}
}
},
"/websites/ca": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建网站 ca",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Create website ca",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCACreate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/request.WebsiteCACreate"
}
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name"
],
"formatEN": "Create website ca [name]",
"formatZH": "创建网站 ca [name]",
"paramKeys": []
}
}
},
"/websites/ca/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除网站 ca",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Delete website ca",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCommonReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "website_cas",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "Delete website ca [name]",
"formatZH": "删除网站 ca [name]",
"paramKeys": []
}
}
},
"/websites/ca/obtain": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "自签 SSL 证书",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Obtain SSL",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCAObtain"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "website_cas",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "Obtain SSL [name]",
"formatZH": "自签 SSL 证书 [name]",
"paramKeys": []
}
}
},
"/websites/ca/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取网站 ca 列表分页",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Page website ca",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCASearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/websites/ca/{id}": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取网站 ca",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Get website ca",
"parameters": [
{
"type": "integer",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.WebsiteCADTO"
}
}
}
}
},
"/websites/check": {
"post": {
"security": [
@ -18667,6 +18884,104 @@ const docTemplate = `{
}
}
},
"request.WebsiteCACreate": {
"type": "object",
"required": [
"commonName",
"country",
"email",
"keyType",
"name",
"organization"
],
"properties": {
"city": {
"type": "string"
},
"commonName": {
"type": "string"
},
"country": {
"type": "string"
},
"email": {
"type": "string"
},
"keyType": {
"type": "string",
"enum": [
"P256",
"P384",
"2048",
"3072",
"4096",
"8192"
]
},
"name": {
"type": "string"
},
"organization": {
"type": "string"
},
"organizationUint": {
"type": "string"
},
"province": {
"type": "string"
}
}
},
"request.WebsiteCAObtain": {
"type": "object",
"required": [
"domains",
"id",
"keyType",
"time",
"unit"
],
"properties": {
"domains": {
"type": "string"
},
"id": {
"type": "integer"
},
"keyType": {
"type": "string",
"enum": [
"P256",
"P384",
"2048",
"3072",
"4096",
"8192"
]
},
"time": {
"type": "integer"
},
"unit": {
"type": "string"
}
}
},
"request.WebsiteCASearch": {
"type": "object",
"required": [
"page",
"pageSize"
],
"properties": {
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
}
}
},
"request.WebsiteCommonReq": {
"type": "object",
"required": [
@ -19793,6 +20108,32 @@ const docTemplate = `{
}
}
},
"response.WebsiteCADTO": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"csr": {
"type": "string"
},
"id": {
"type": "integer"
},
"keyType": {
"type": "string"
},
"name": {
"type": "string"
},
"privateKey": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"response.WebsiteDNSRes": {
"type": "object",
"properties": {

View File

@ -11009,6 +11009,223 @@
}
}
},
"/websites/ca": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建网站 ca",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Create website ca",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCACreate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/request.WebsiteCACreate"
}
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name"
],
"formatEN": "Create website ca [name]",
"formatZH": "创建网站 ca [name]",
"paramKeys": []
}
}
},
"/websites/ca/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除网站 ca",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Delete website ca",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCommonReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "website_cas",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "Delete website ca [name]",
"formatZH": "删除网站 ca [name]",
"paramKeys": []
}
}
},
"/websites/ca/obtain": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "自签 SSL 证书",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Obtain SSL",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCAObtain"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "website_cas",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "Obtain SSL [name]",
"formatZH": "自签 SSL 证书 [name]",
"paramKeys": []
}
}
},
"/websites/ca/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取网站 ca 列表分页",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Page website ca",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCASearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/websites/ca/{id}": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取网站 ca",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Get website ca",
"parameters": [
{
"type": "integer",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.WebsiteCADTO"
}
}
}
}
},
"/websites/check": {
"post": {
"security": [
@ -18660,6 +18877,104 @@
}
}
},
"request.WebsiteCACreate": {
"type": "object",
"required": [
"commonName",
"country",
"email",
"keyType",
"name",
"organization"
],
"properties": {
"city": {
"type": "string"
},
"commonName": {
"type": "string"
},
"country": {
"type": "string"
},
"email": {
"type": "string"
},
"keyType": {
"type": "string",
"enum": [
"P256",
"P384",
"2048",
"3072",
"4096",
"8192"
]
},
"name": {
"type": "string"
},
"organization": {
"type": "string"
},
"organizationUint": {
"type": "string"
},
"province": {
"type": "string"
}
}
},
"request.WebsiteCAObtain": {
"type": "object",
"required": [
"domains",
"id",
"keyType",
"time",
"unit"
],
"properties": {
"domains": {
"type": "string"
},
"id": {
"type": "integer"
},
"keyType": {
"type": "string",
"enum": [
"P256",
"P384",
"2048",
"3072",
"4096",
"8192"
]
},
"time": {
"type": "integer"
},
"unit": {
"type": "string"
}
}
},
"request.WebsiteCASearch": {
"type": "object",
"required": [
"page",
"pageSize"
],
"properties": {
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
}
}
},
"request.WebsiteCommonReq": {
"type": "object",
"required": [
@ -19786,6 +20101,32 @@
}
}
},
"response.WebsiteCADTO": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"csr": {
"type": "string"
},
"id": {
"type": "integer"
},
"keyType": {
"type": "string"
},
"name": {
"type": "string"
},
"privateKey": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"response.WebsiteDNSRes": {
"type": "object",
"properties": {

View File

@ -3710,6 +3710,77 @@ definitions:
- keyType
- type
type: object
request.WebsiteCACreate:
properties:
city:
type: string
commonName:
type: string
country:
type: string
email:
type: string
keyType:
enum:
- P256
- P384
- "2048"
- "3072"
- "4096"
- "8192"
type: string
name:
type: string
organization:
type: string
organizationUint:
type: string
province:
type: string
required:
- commonName
- country
- email
- keyType
- name
- organization
type: object
request.WebsiteCAObtain:
properties:
domains:
type: string
id:
type: integer
keyType:
enum:
- P256
- P384
- "2048"
- "3072"
- "4096"
- "8192"
type: string
time:
type: integer
unit:
type: string
required:
- domains
- id
- keyType
- time
- unit
type: object
request.WebsiteCASearch:
properties:
page:
type: integer
pageSize:
type: integer
required:
- page
- pageSize
type: object
request.WebsiteCommonReq:
properties:
id:
@ -4461,6 +4532,23 @@ definitions:
url:
type: string
type: object
response.WebsiteCADTO:
properties:
createdAt:
type: string
csr:
type: string
id:
type: integer
keyType:
type: string
name:
type: string
privateKey:
type: string
updatedAt:
type: string
type: object
response.WebsiteDNSRes:
properties:
domain:
@ -11567,6 +11655,144 @@ paths:
summary: Get AuthBasic conf
tags:
- Website
/websites/ca:
post:
consumes:
- application/json
description: 创建网站 ca
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteCACreate'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/request.WebsiteCACreate'
security:
- ApiKeyAuth: []
summary: Create website ca
tags:
- Website CA
x-panel-log:
BeforeFunctions: []
bodyKeys:
- name
formatEN: Create website ca [name]
formatZH: 创建网站 ca [name]
paramKeys: []
/websites/ca/{id}:
get:
consumes:
- application/json
description: 获取网站 ca
parameters:
- description: id
in: path
name: id
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.WebsiteCADTO'
security:
- ApiKeyAuth: []
summary: Get website ca
tags:
- Website CA
/websites/ca/del:
post:
consumes:
- application/json
description: 删除网站 ca
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteCommonReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Delete website ca
tags:
- Website CA
x-panel-log:
BeforeFunctions:
- db: website_cas
input_column: id
input_value: id
isList: false
output_column: name
output_value: name
bodyKeys:
- id
formatEN: Delete website ca [name]
formatZH: 删除网站 ca [name]
paramKeys: []
/websites/ca/obtain:
post:
consumes:
- application/json
description: 自签 SSL 证书
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteCAObtain'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Obtain SSL
tags:
- Website CA
x-panel-log:
BeforeFunctions:
- db: website_cas
input_column: id
input_value: id
isList: false
output_column: name
output_value: name
bodyKeys:
- id
formatEN: Obtain SSL [name]
formatZH: 自签 SSL 证书 [name]
paramKeys: []
/websites/ca/search:
post:
consumes:
- application/json
description: 获取网站 ca 列表分页
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteCASearch'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Page website ca
tags:
- Website CA
/websites/check:
post:
consumes:

View File

@ -456,4 +456,31 @@ export namespace Website {
export interface SSLObtain {
ID: number;
}
export interface CA extends CommonModel {
name: string;
csr: string;
privateKey: string;
keyType: string;
}
export interface CACreate {
name: string;
commonName: string;
country: string;
email: string;
organization: string;
organizationUint: string;
keyType: string;
province: string;
city: string;
}
export interface SSLObtainByCA {
id: number;
domains: string;
keyType: string;
time: number;
unit: string;
}
}

View File

@ -247,3 +247,19 @@ export const GetDirConfig = (req: Website.ProxyReq) => {
export const UploadSSL = (req: Website.SSLUpload) => {
return http.post<any>(`/websites/ssl/upload`, req);
};
export const SearchCAs = (req: ReqPage) => {
return http.post<ResPage<Website.CA>>(`/websites/ca/search`, req);
};
export const CreateCA = (req: Website.CACreate) => {
return http.post<Website.CA>(`/websites/ca`, req);
};
export const ObtainSSLByCA = (req: Website.SSLObtainByCA) => {
return http.post<any>(`/websites/ca/obtain`, req);
};
export const DeleteCA = (req: Website.DelReq) => {
return http.post<any>(`/websites/ca/del`, req);
};

View File

@ -132,7 +132,7 @@ const checkName = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.commonName')));
} else {
const reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-zA-Z0-9_.\u4e00-\u9fa5-]{0,29}$/;
const reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-zA-Z0-9_.\u4e00-\u9fa5-]{0,128}$/;
if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.commonName')));
} else {

View File

@ -137,3 +137,34 @@ export const KeyTypes = [
{ label: 'RSA 3072', value: '3072' },
{ label: 'RSA 4096', value: '4096' },
];
export const DNSTypes = [
{
label: i18n.global.t('website.aliyun'),
value: 'AliYun',
},
{
label: 'DNSPod',
value: 'DnsPod',
},
{
label: 'CloudFlare',
value: 'CloudFlare',
},
{
label: 'NameSilo',
value: 'NameSilo',
},
{
label: 'NameCheap',
value: 'NameCheap',
},
{
label: 'Name.com',
value: 'NameCom',
},
{
label: 'Godaddy',
value: 'Godaddy',
},
];

View File

@ -156,7 +156,7 @@ const message = {
requiredInput: 'Please enter the required fields',
requiredSelect: 'Please select the required fields',
illegalInput: 'There are illegal characters in the input box.',
commonName: 'Support English, Chinese, numbers, .-, and _ length 1-30',
commonName: 'Support English, Chinese, numbers, .-, and _ length 1-128',
userName: 'Support English, Chinese, numbers and _ length 3-30',
simpleName: 'Support English, numbers and _ length 1-30',
dbName: 'Support English, Chinese, numbers, .-, and _ length 1-64',
@ -1557,7 +1557,7 @@ const message = {
provider: 'Verification method',
dnsManual: 'Manual resolution',
expireDate: 'Expiration Time',
brand: 'Issuer',
brand: 'Organization',
deploySSL: 'Deployment',
deploySSLHelper: 'Are you sure to deploy the certificate? ',
ssl: 'Certificate',
@ -1822,6 +1822,20 @@ const message = {
apply: 'Apply',
applyStart: 'Certificate application starts',
getDnsResolve: 'Getting DNS resolution value, please wait...',
selfSigned: 'Self-signed certificate',
ca: 'Certification Authority',
createCA: 'Create institution',
commonName: 'Certificate subject name (CN)',
caName: 'Institution name',
company: 'company/organization',
department: 'department',
city: 'city',
province: 'province',
country: 'country code',
commonNameHelper: 'For example:',
selfSign: 'Issue certificate',
days: 'validity period',
domainHelper: 'One domain name per line, supports * and IP address',
},
firewall: {
create: 'Create rule',

View File

@ -157,7 +157,7 @@ const message = {
requiredInput: '',
requiredSelect: '',
illegalInput: '',
commonName: '.-_,1-30',
commonName: '.-_,1-128',
userName: '_,3-30',
simpleName: '_,1-30',
dbName: '.-_,1-64',
@ -1458,7 +1458,7 @@ const message = {
provider: '',
dnsManual: '',
expireDate: '',
brand: '',
brand: '',
deploySSL: '',
deploySSLHelper: '',
ssl: '',
@ -1710,6 +1710,20 @@ const message = {
apply: '',
applyStart: '',
getDnsResolve: ' DNS , ...',
selfSigned: '',
ca: '',
createCA: '',
commonName: '(CN)',
caName: '',
company: '/',
department: '',
city: '',
province: '',
country: '',
commonNameHelper: ':',
selfSign: '',
days: '',
domainHelper: ',*IP',
},
firewall: {
create: '',

View File

@ -157,7 +157,7 @@ const message = {
requiredInput: '',
requiredSelect: '',
illegalInput: '',
commonName: '.-_,1-30',
commonName: '.-_,1-128',
userName: '_,3-30',
simpleName: '_,1-30',
dbName: '.-_,1-64',
@ -1458,7 +1458,7 @@ const message = {
provider: '',
dnsManual: '',
expireDate: '',
brand: '',
brand: '',
deploySSL: '',
deploySSLHelper: '',
ssl: '',
@ -1710,6 +1710,20 @@ const message = {
apply: '',
applyStart: '',
getDnsResolve: ' DNS , ...',
selfSigned: '',
ca: '',
createCA: '',
commonName: '(CN)',
caName: '',
company: '/',
department: '',
city: '',
province: '',
country: '',
commonNameHelper: ':',
selfSign: '',
days: '',
domainHelper: ',*IP',
},
firewall: {
create: '',

View File

@ -1,3 +1,4 @@
import { AcmeAccountTypes, DNSTypes, KeyTypes } from '@/global/mimetype';
import i18n from '@/lang';
export function deepCopy<T>(obj: any): T {
@ -322,6 +323,8 @@ export function getProvider(provider: string): string {
return i18n.global.t('website.dnsManual');
case 'http':
return 'HTTP';
case 'selfSigned':
return i18n.global.t('ssl.selfSigned');
default:
return i18n.global.t('ssl.manualCreate');
}
@ -437,3 +440,30 @@ export function getDateStr() {
return timestamp;
}
export function getAccountName(type: string) {
for (const i of AcmeAccountTypes) {
if (i.value === type) {
return i.label;
}
}
return '';
}
export function getKeyName(type: string) {
for (const i of KeyTypes) {
if (i.value === type) {
return i.label;
}
}
return '';
}
export function getDNSName(type: string) {
for (const i of DNSTypes) {
if (i.value === type) {
return i.label;
}
}
return '';
}

View File

@ -19,12 +19,12 @@
></el-table-column>
<el-table-column :label="$t('website.acmeAccountType')" fix show-overflow-tooltip prop="type">
<template #default="{ row }">
{{ getAccountType(row.type) }}
{{ getAccountName(row.type) }}
</template>
</el-table-column>
<el-table-column :label="$t('website.keyType')" fix show-overflow-tooltip prop="keyType">
<template #default="{ row }">
{{ getKeyType(row.keyType) }}
{{ getKeyName(row.keyType) }}
</template>
</el-table-column>
<el-table-column label="URL" show-overflow-tooltip prop="url" min-width="300px"></el-table-column>
@ -38,7 +38,6 @@
</ComplexTable>
<Create ref="createRef" @close="search()"></Create>
</el-drawer>
<OpDialog ref="opRef" @search="search" />
</template>
@ -50,7 +49,7 @@ import { DeleteAcmeAccount, SearchAcmeAccount } from '@/api/modules/website';
import i18n from '@/lang';
import { reactive, ref } from 'vue';
import Create from './create/index.vue';
import { AcmeAccountTypes, KeyTypes } from '@/global/mimetype';
import { getAccountName, getKeyName } from '@/utils/util';
const open = ref(false);
const loading = ref(false);
@ -110,21 +109,6 @@ const deleteAccount = async (row: any) => {
});
};
const getAccountType = (type: string) => {
for (const i of AcmeAccountTypes) {
if (i.value === type) {
return i.label;
}
}
};
const getKeyType = (type: string) => {
for (const i of KeyTypes) {
if (i.value === type) {
return i.label;
}
}
};
defineExpose({
acceptParams,
});

View File

@ -0,0 +1,134 @@
<template>
<el-dialog
v-model="open"
:title="$t('ssl.createCA')"
:close-on-click-modal="false"
width="40%"
:before-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('website.email')" prop="email">
<el-input v-model.trim="ca.email"></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>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(caForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { KeyTypes } from '@/global/mimetype';
import { CreateCA } from '@/api/modules/website';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
const open = ref(false);
const loading = ref(false);
const caForm = ref<FormInstance>();
const em = defineEmits(['close']);
const rules = ref({
email: [Rules.requiredInput, Rules.email],
keyType: [Rules.requiredSelect],
name: [Rules.requiredInput, Rules.name],
country: [Rules.requiredSelect],
organization: [Rules.requiredInput],
commonName: [Rules.requiredInput],
});
const initData = () => ({
name: '',
email: '',
keyType: 'P256',
commonName: '',
country: 'CN',
organization: '',
organizationUint: '',
province: '',
city: '',
});
const ca = ref(initData());
const handleClose = () => {
open.value = false;
em('close', false);
resetForm();
};
const resetForm = () => {
caForm.value.resetFields();
ca.value = initData();
};
const acceptParams = () => {
open.value = true;
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
CreateCA(ca.value)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -0,0 +1,121 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="open" size="50%">
<template #header>
<DrawerHeader :header="$t('ssl.ca')" :back="handleClose" />
</template>
<ComplexTable :data="data" :pagination-config="paginationConfig" @search="search()" v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="openCreate">{{ $t('ssl.createCA') }}</el-button>
</template>
<el-table-column
:label="$t('commons.table.name')"
fix
show-overflow-tooltip
prop="name"
min-width="100px"
></el-table-column>
<el-table-column :label="$t('website.keyType')" fix show-overflow-tooltip prop="keyType">
<template #default="{ row }">
{{ getKeyName(row.keyType) }}
</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()" />
<Obtain ref="obtainRef" @close="handleClose()" />
</el-drawer>
<OpDialog ref="opRef" @search="search" />
</template>
<script lang="ts" setup>
import OpDialog from '@/components/del-dialog/index.vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { Website } from '@/api/interface/website';
import { DeleteCA, SearchCAs } from '@/api/modules/website';
import i18n from '@/lang';
import { reactive, ref } from 'vue';
import Create from './create/index.vue';
import { getKeyName } from '@/utils/util';
import Obtain from './obtain/index.vue';
const open = ref(false);
const loading = ref(false);
const data = ref();
const createRef = ref();
const paginationConfig = reactive({
cacheSizeKey: 'ca-page-size',
currentPage: 1,
pageSize: 20,
total: 0,
});
const opRef = ref();
const obtainRef = ref();
const em = defineEmits(['close']);
const buttons = [
{
label: i18n.global.t('ssl.selfSign'),
click: function (row: Website.CA) {
obtain(row);
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Website.CA) {
deleteCA(row);
},
},
];
const acceptParams = () => {
search();
open.value = true;
};
const obtain = (row: any) => {
obtainRef.value.acceptParams(row.id);
};
const search = async () => {
const req = {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
await SearchCAs(req).then((res) => {
data.value = res.data.items;
paginationConfig.total = res.data.total;
});
};
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'),
names: [row.name],
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('website.ca'),
i18n.global.t('commons.button.delete'),
]),
api: DeleteCA,
params: { id: row.id },
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -0,0 +1,121 @@
<template>
<el-dialog
v-model="open"
:title="$t('ssl.selfSigned')"
:close-on-click-modal="false"
width="40%"
:before-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"
:autosize="{ minRows: 4, maxRows: 10 }"
v-model="obtain.domains"
:placeholder="$t('website.domainHelper')"
></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>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(obtainForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ObtainSSLByCA } from '@/api/modules/website';
import { Rules, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { KeyTypes } from '@/global/mimetype';
const open = ref(false);
const loading = ref(false);
const obtainForm = ref<FormInstance>();
const em = defineEmits(['close']);
const rules = ref({
keyType: [Rules.requiredSelect],
domains: [Rules.requiredInput],
time: [Rules.requiredInput, checkNumberRange(1, 1000)],
});
const initData = () => ({
keyType: 'P256',
domains: '',
id: 0,
time: 0,
unit: 'day',
});
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 submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
ObtainSSLByCA(obtain.value)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -52,9 +52,20 @@
<el-option
v-for="(dns, index) in dnsAccounts"
:key="index"
:label="dns.name + ' ( ' + dns.type + ' )'"
:label="dns.name"
:value="dns.id"
></el-option>
>
<el-row>
<el-col :span="6">
<span>{{ dns.name }}</span>
</el-col>
<el-col :span="11">
<span>
<el-tag type="success">{{ dns.type }}</el-tag>
</span>
</el-col>
</el-row>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="''" prop="autoRenew" v-if="ssl.provider !== 'dnsManual'">

View File

@ -28,7 +28,7 @@
>
{{ ssl.acmeAccount.email }}
</el-descriptions-item>
<el-descriptions-item :label="$t('commons.table.type')">
<el-descriptions-item :label="$t('ssl.commonName')">
{{ ssl.type }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.brand')">

View File

@ -16,7 +16,7 @@
<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 types"
v-for="(type, index) in DNSTypes"
:key="index"
:label="type.label"
:value="type.value"
@ -92,6 +92,7 @@ import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { DNSTypes } from '@/global/mimetype';
interface AccountProps {
mode: string;
@ -102,37 +103,6 @@ const accountData = ref<AccountProps>({
form: {},
});
const types = [
{
label: i18n.global.t('website.aliyun'),
value: 'AliYun',
},
{
label: 'DNSPod',
value: 'DnsPod',
},
{
label: 'CloudFlare',
value: 'CloudFlare',
},
{
label: 'NameSilo',
value: 'NameSilo',
},
{
label: 'NameCheap',
value: 'NameCheap',
},
{
label: 'Name.com',
value: 'NameCom',
},
{
label: 'Godaddy',
value: 'Godaddy',
},
];
const open = ref();
const loading = ref(false);
const accountForm = ref<FormInstance>();

View File

@ -12,8 +12,7 @@
<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 v-if="row.type == 'AliYun'">{{ $t('website.aliyun') }}</span>
<span v-else>{{ row.type }}</span>
<span>{{ getDNSName(row.type) }}</span>
</template>
</el-table-column>
<fu-table-operations
@ -37,6 +36,7 @@ import { Website } from '@/api/interface/website';
import { DeleteDnsAccount, SearchDnsAccount } from '@/api/modules/website';
import { onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { getDNSName } from '@/utils/util';
const paginationConfig = reactive({
cacheSizeKey: 'dns-account-page-size',

View File

@ -16,6 +16,9 @@
<el-button type="primary" @click="openUpload()">
{{ $t('ssl.upload') }}
</el-button>
<el-button type="primary" plain @click="openCA()">
{{ $t('ssl.selfSigned') }}
</el-button>
<el-button type="primary" plain @click="openAcmeAccount()">
{{ $t('website.acmeAccountManage') }}
</el-button>
@ -95,7 +98,11 @@
<el-table-column :label="$t('ssl.autoRenew')" fix width="100px">
<template #default="{ row }">
<el-switch
:disabled="row.provider === 'dnsManual' || row.provider === 'manual'"
:disabled="
row.provider === 'dnsManual' ||
row.provider === 'manual' ||
row.provider === 'selfSigned'
"
v-model="row.autoRenew"
@change="updateConfig(row)"
/>
@ -124,6 +131,7 @@
<Apply ref="applyRef" @search="search" />
<OpDialog ref="opRef" @search="search" />
<Log ref="logRef" @close="search()" />
<CA ref="caRef" @close="search()" />
</LayoutContent>
</div>
</template>
@ -134,6 +142,7 @@ import OpDialog from '@/components/del-dialog/index.vue';
import { DeleteSSL, ObtainSSL, 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 Create from './create/index.vue';
import Detail from './detail/index.vue';
import { dateFormat, getProvider } from '@/utils/util';
@ -162,6 +171,7 @@ const opRef = ref();
const sslUploadRef = ref();
const applyRef = ref();
const logRef = ref();
const caRef = ref();
const routerButton = [
{
@ -183,7 +193,7 @@ const buttons = [
{
label: i18n.global.t('ssl.apply'),
disabled: function (row: Website.SSLDTO) {
return row.status === 'applying';
return row.status === 'applying' || row.provider === 'manual' || row.provider === 'selfSigned';
},
click: function (row: Website.SSLDTO) {
if (row.provider === 'dnsManual') {
@ -250,6 +260,9 @@ const openDetail = (id: number) => {
const openLog = (row: Website.SSLDTO) => {
logRef.value.acceptParams({ id: row.id, type: 'ssl' });
};
const openCA = () => {
caRef.value.acceptParams();
};
const applySSL = (row: Website.SSLDTO) => {
loading.value = true;