diff --git a/backend/app/api/v1/entry.go b/backend/app/api/v1/entry.go index d7c52e54e..78adffc55 100644 --- a/backend/app/api/v1/entry.go +++ b/backend/app/api/v1/entry.go @@ -59,4 +59,6 @@ var ( recycleBinService = service.NewIRecycleBinService() favoriteService = service.NewIFavoriteService() + + websiteCAService = service.NewIWebsiteCAService() ) diff --git a/backend/app/api/v1/website_ca.go b/backend/app/api/v1/website_ca.go new file mode 100644 index 000000000..3dabf736a --- /dev/null +++ b/backend/app/api/v1/website_ca.go @@ -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) +} diff --git a/backend/app/dto/request/website_ssl.go b/backend/app/dto/request/website_ssl.go index 3d201e929..a66eef523 100644 --- a/backend/app/dto/request/website_ssl.go +++ b/backend/app/dto/request/website_ssl.go @@ -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"` +} diff --git a/backend/app/dto/response/website_ssl.go b/backend/app/dto/response/website_ssl.go index c501536e5..167bd6a10 100644 --- a/backend/app/dto/response/website_ssl.go +++ b/backend/app/dto/response/website_ssl.go @@ -22,3 +22,7 @@ type WebsiteDnsAccountDTO struct { model.WebsiteDnsAccount Authorization map[string]string `json:"authorization"` } + +type WebsiteCADTO struct { + model.WebsiteCA +} diff --git a/backend/app/model/website_ca.go b/backend/app/model/website_ca.go new file mode 100644 index 000000000..74b029637 --- /dev/null +++ b/backend/app/model/website_ca.go @@ -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"` +} diff --git a/backend/app/repo/website_ca.go b/backend/app/repo/website_ca.go new file mode 100644 index 000000000..d9c9ae5f1 --- /dev/null +++ b/backend/app/repo/website_ca.go @@ -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 +} diff --git a/backend/app/service/entry.go b/backend/app/service/entry.go index a9a1ebbc9..1308248cb 100644 --- a/backend/app/service/entry.go +++ b/backend/app/service/entry.go @@ -32,6 +32,7 @@ var ( websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo() websiteSSLRepo = repo.NewISSLRepo() websiteAcmeRepo = repo.NewIAcmeAccountRepo() + websiteCARepo = repo.NewIWebsiteCARepo() logRepo = repo.NewILogRepo() snapshotRepo = repo.NewISnapshotRepo() diff --git a/backend/app/service/website_ca.go b/backend/app/service/website_ca.go new file mode 100644 index 000000000..be6183e85 --- /dev/null +++ b/backend/app/service/website_ca.go @@ -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 +} diff --git a/backend/app/service/website_ssl.go b/backend/app/service/website_ssl.go index b237606d2..659e1bd78 100644 --- a/backend/app/service/website_ssl.go +++ b/backend/app/service/website_ssl.go @@ -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, ",") diff --git a/backend/constant/website.go b/backend/constant/website.go index fbad8a6ec..306a7d488 100644 --- a/backend/constant/website.go +++ b/backend/constant/website.go @@ -27,6 +27,7 @@ const ( DnsManual = "dnsManual" Http = "http" Manual = "manual" + SelfSigned = "selfSigned" StartWeb = "start" StopWeb = "stop" diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 57da7f746..265509494 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -55,6 +55,7 @@ func Init() { migrations.UpdateAcmeAccount, migrations.UpdateWebsiteSSL, + migrations.AddWebsiteCA, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/v_1_9.go b/backend/init/migration/migrations/v_1_9.go index cb74a944d..d8916339d 100644 --- a/backend/init/migration/migrations/v_1_9.go +++ b/backend/init/migration/migrations/v_1_9.go @@ -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 + }, +} diff --git a/backend/init/router/router.go b/backend/init/router/router.go index a601a50eb..d980ef6f4 100644 --- a/backend/init/router/router.go +++ b/backend/init/router/router.go @@ -96,6 +96,7 @@ func Routers() *gin.Engine { systemRouter.InitRuntimeRouter(PrivateGroup) systemRouter.InitProcessRouter(PrivateGroup) systemRouter.InitToolboxRouter(PrivateGroup) + systemRouter.InitWebsiteCARouter(PrivateGroup) } return Router diff --git a/backend/router/ro_website_ca.go b/backend/router/ro_website_ca.go new file mode 100644 index 000000000..a5df56baa --- /dev/null +++ b/backend/router/ro_website_ca.go @@ -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) + } +} diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 063f2edea..0d4c9b065 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -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": { diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 41fa03ad9..4b6fc241b 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -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": { diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index cd19f432f..2b7a80b2e 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -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: diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index e032b889b..f027fbde2 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -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; + } } diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index 762c5e10d..ee5a29f2d 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -247,3 +247,19 @@ export const GetDirConfig = (req: Website.ProxyReq) => { export const UploadSSL = (req: Website.SSLUpload) => { return http.post(`/websites/ssl/upload`, req); }; + +export const SearchCAs = (req: ReqPage) => { + return http.post>(`/websites/ca/search`, req); +}; + +export const CreateCA = (req: Website.CACreate) => { + return http.post(`/websites/ca`, req); +}; + +export const ObtainSSLByCA = (req: Website.SSLObtainByCA) => { + return http.post(`/websites/ca/obtain`, req); +}; + +export const DeleteCA = (req: Website.DelReq) => { + return http.post(`/websites/ca/del`, req); +}; diff --git a/frontend/src/global/form-rules.ts b/frontend/src/global/form-rules.ts index 1c5cbf1c4..175f546a3 100644 --- a/frontend/src/global/form-rules.ts +++ b/frontend/src/global/form-rules.ts @@ -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 { diff --git a/frontend/src/global/mimetype.ts b/frontend/src/global/mimetype.ts index 7eb8d09f0..eb80ab793 100644 --- a/frontend/src/global/mimetype.ts +++ b/frontend/src/global/mimetype.ts @@ -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', + }, +]; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 865dc7890..7e464d87b 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 20c452c12..223cef77c 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -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: '創建規則', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 908500d43..f0393cbf1 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '创建规则', diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts index 48190ba8c..dfb887fde 100644 --- a/frontend/src/utils/util.ts +++ b/frontend/src/utils/util.ts @@ -1,3 +1,4 @@ +import { AcmeAccountTypes, DNSTypes, KeyTypes } from '@/global/mimetype'; import i18n from '@/lang'; export function deepCopy(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 ''; +} diff --git a/frontend/src/views/website/ssl/acme-account/index.vue b/frontend/src/views/website/ssl/acme-account/index.vue index 86f5ef777..6463a2212 100644 --- a/frontend/src/views/website/ssl/acme-account/index.vue +++ b/frontend/src/views/website/ssl/acme-account/index.vue @@ -19,12 +19,12 @@ > @@ -38,7 +38,6 @@ - @@ -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, }); diff --git a/frontend/src/views/website/ssl/ca/create/index.vue b/frontend/src/views/website/ssl/ca/create/index.vue new file mode 100644 index 000000000..98808edf2 --- /dev/null +++ b/frontend/src/views/website/ssl/ca/create/index.vue @@ -0,0 +1,134 @@ + + + diff --git a/frontend/src/views/website/ssl/ca/index.vue b/frontend/src/views/website/ssl/ca/index.vue new file mode 100644 index 000000000..4fe573f2b --- /dev/null +++ b/frontend/src/views/website/ssl/ca/index.vue @@ -0,0 +1,121 @@ + + + diff --git a/frontend/src/views/website/ssl/ca/obtain/index.vue b/frontend/src/views/website/ssl/ca/obtain/index.vue new file mode 100644 index 000000000..4ff67c1e7 --- /dev/null +++ b/frontend/src/views/website/ssl/ca/obtain/index.vue @@ -0,0 +1,121 @@ + + + diff --git a/frontend/src/views/website/ssl/create/index.vue b/frontend/src/views/website/ssl/create/index.vue index 1502e2b40..91ad14d40 100644 --- a/frontend/src/views/website/ssl/create/index.vue +++ b/frontend/src/views/website/ssl/create/index.vue @@ -52,9 +52,20 @@ + > + + + {{ dns.name }} + + + + {{ dns.type }} + + + + diff --git a/frontend/src/views/website/ssl/detail/index.vue b/frontend/src/views/website/ssl/detail/index.vue index bfd0008fa..b57b26c2f 100644 --- a/frontend/src/views/website/ssl/detail/index.vue +++ b/frontend/src/views/website/ssl/detail/index.vue @@ -28,7 +28,7 @@ > {{ ssl.acmeAccount.email }} - + {{ ssl.type }} diff --git a/frontend/src/views/website/ssl/dns-account/create/index.vue b/frontend/src/views/website/ssl/dns-account/create/index.vue index 81cad65a3..6ddc08d36 100644 --- a/frontend/src/views/website/ssl/dns-account/create/index.vue +++ b/frontend/src/views/website/ssl/dns-account/create/index.vue @@ -16,7 +16,7 @@ ({ 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(); diff --git a/frontend/src/views/website/ssl/dns-account/index.vue b/frontend/src/views/website/ssl/dns-account/index.vue index 45201c591..fa8a8a97e 100644 --- a/frontend/src/views/website/ssl/dns-account/index.vue +++ b/frontend/src/views/website/ssl/dns-account/index.vue @@ -12,8 +12,7 @@ {{ $t('ssl.upload') }} + + {{ $t('ssl.selfSigned') }} + {{ $t('website.acmeAccountManage') }} @@ -95,7 +98,11 @@ @@ -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;