mirror of
				https://github.com/1Panel-dev/1Panel.git
				synced 2025-11-01 03:37:19 +08:00 
			
		
		
		
	feat: 优化网站域名添加 (#6069)
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				sync2gitee / repo-sync (push) Failing after -7m36s
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	sync2gitee / repo-sync (push) Failing after -7m36s
				
			This commit is contained in:
		
							parent
							
								
									94b5a7fae4
								
							
						
					
					
						commit
						a225d2d79a
					
				
					 16 changed files with 313 additions and 185 deletions
				
			
		|  | @ -13,15 +13,15 @@ type WebsiteSearch struct { | |||
| } | ||||
| 
 | ||||
| type WebsiteCreate struct { | ||||
| 	PrimaryDomain  string `json:"primaryDomain" validate:"required"` | ||||
| 	Type           string `json:"type" validate:"required"` | ||||
| 	Alias          string `json:"alias" validate:"required"` | ||||
| 	Remark         string `json:"remark"` | ||||
| 	OtherDomains   string `json:"otherDomains"` | ||||
| 	Proxy          string `json:"proxy"` | ||||
| 	WebsiteGroupID uint   `json:"webSiteGroupID" validate:"required"` | ||||
| 	IPV6           bool   `json:"IPV6"` | ||||
| 
 | ||||
| 	Domains []WebsiteDomain `json:"domains"` | ||||
| 
 | ||||
| 	AppType      string        `json:"appType" validate:"oneof=new installed"` | ||||
| 	AppInstall   NewAppInstall `json:"appInstall"` | ||||
| 	AppID        uint          `json:"appID"` | ||||
|  | @ -123,8 +123,14 @@ type WebsiteGroupUpdate struct { | |||
| } | ||||
| 
 | ||||
| type WebsiteDomainCreate struct { | ||||
| 	WebsiteID uint   `json:"websiteID" validate:"required"` | ||||
| 	Domains   string `json:"domains" validate:"required"` | ||||
| 	WebsiteID uint            `json:"websiteID" validate:"required"` | ||||
| 	Domains   []WebsiteDomain `json:"domains" validate:"required"` | ||||
| } | ||||
| 
 | ||||
| type WebsiteDomain struct { | ||||
| 	Domain string `json:"domain" validate:"required"` | ||||
| 	Port   int    `json:"port"` | ||||
| 	SSL    bool   `json:"SSL"` | ||||
| } | ||||
| 
 | ||||
| type WebsiteDomainDelete struct { | ||||
|  | @ -145,7 +151,7 @@ type WebsiteHTTPSOp struct { | |||
| 	SSLProtocol     []string `json:"SSLProtocol"` | ||||
| 	Algorithm       string   `json:"algorithm"` | ||||
| 	Hsts            bool     `json:"hsts"` | ||||
| 	HttpsPort       int      `json:"httpsPort"` | ||||
| 	HttpsPorts      []int    `json:"httpsPorts"` | ||||
| } | ||||
| 
 | ||||
| type WebsiteNginxUpdate struct { | ||||
|  |  | |||
|  | @ -59,7 +59,8 @@ type WebsiteHTTPS struct { | |||
| 	SSLProtocol []string         `json:"SSLProtocol"` | ||||
| 	Algorithm   string           `json:"algorithm"` | ||||
| 	Hsts        bool             `json:"hsts"` | ||||
| 	HttpsPort   int              `json:"httpsPort"` | ||||
| 	HttpsPorts  []int            `json:"httpsPorts"` | ||||
| 	HttpsPort   string           `json:"httpsPort"` | ||||
| } | ||||
| 
 | ||||
| type WebsiteLog struct { | ||||
|  |  | |||
|  | @ -28,8 +28,6 @@ type Website struct { | |||
| 	AppInstallID   uint `gorm:"type:integer" json:"appInstallId"` | ||||
| 	FtpID          uint `gorm:"type:integer" json:"ftpId"` | ||||
| 
 | ||||
| 	HttpsPort int `json:"httpsPort"` | ||||
| 
 | ||||
| 	User  string `gorm:"type:varchar;" json:"user"` | ||||
| 	Group string `gorm:"type:varchar;" json:"group"` | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ type WebsiteDomain struct { | |||
| 	BaseModel | ||||
| 	WebsiteID uint   `gorm:"column:website_id;type:varchar(64);not null;" json:"websiteId"` | ||||
| 	Domain    string `gorm:"type:varchar(256);not null" json:"domain"` | ||||
| 	SSL       bool   `json:"SSL"` | ||||
| 	Port      int    `gorm:"type:integer" json:"port"` | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -225,28 +225,21 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) | |||
| 		return err | ||||
| 	} | ||||
| 	defaultHttpPort := nginxInstall.HttpPort | ||||
| 	defaultHttpsPort := nginxInstall.HttpsPort | ||||
| 
 | ||||
| 	var ( | ||||
| 		otherDomains []model.WebsiteDomain | ||||
| 		domains      []model.WebsiteDomain | ||||
| 		domains []model.WebsiteDomain | ||||
| 	) | ||||
| 	domains, _, _, err = getWebsiteDomains(create.PrimaryDomain, defaultHttpPort, 0) | ||||
| 	domains, _, _, err = getWebsiteDomains(create.Domains, defaultHttpPort, 0) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	otherDomains, _, _, err = getWebsiteDomains(create.OtherDomains, defaultHttpPort, 0) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	domains = append(domains, otherDomains...) | ||||
| 	if len(domains) == 1 && domains[0].Port != defaultHttpPort { | ||||
| 		defaultHttpsPort = domains[0].Port | ||||
| 	primaryDomain := domains[0].Domain | ||||
| 	if domains[0].Port != defaultHttpPort { | ||||
| 		primaryDomain = fmt.Sprintf("%s:%v", domains[0].Domain, domains[0].Port) | ||||
| 	} | ||||
| 
 | ||||
| 	defaultDate, _ := time.Parse(constant.DateLayout, constant.DefaultDate) | ||||
| 	website := &model.Website{ | ||||
| 		PrimaryDomain:  create.PrimaryDomain, | ||||
| 		PrimaryDomain:  primaryDomain, | ||||
| 		Type:           create.Type, | ||||
| 		Alias:          alias, | ||||
| 		Remark:         create.Remark, | ||||
|  | @ -259,7 +252,6 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) | |||
| 		AccessLog:      true, | ||||
| 		ErrorLog:       true, | ||||
| 		IPV6:           create.IPV6, | ||||
| 		HttpsPort:      defaultHttpsPort, | ||||
| 	} | ||||
| 
 | ||||
| 	var ( | ||||
|  | @ -267,7 +259,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) | |||
| 		runtime    *model.Runtime | ||||
| 	) | ||||
| 
 | ||||
| 	createTask, err := task.NewTaskWithOps(create.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID, 0) | ||||
| 	createTask, err := task.NewTaskWithOps(primaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID, 0) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -464,7 +456,6 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) | |||
| 				SSLProtocol:  []string{"TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1"}, | ||||
| 				Algorithm:    "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED", | ||||
| 				Hsts:         true, | ||||
| 				HttpsPort:    website.HttpsPort, | ||||
| 			} | ||||
| 			if err = applySSL(website, *websiteModel, appSSLReq); err != nil { | ||||
| 				return err | ||||
|  | @ -607,7 +598,6 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) | |||
| 	var ( | ||||
| 		domainModels []model.WebsiteDomain | ||||
| 		addPorts     []int | ||||
| 		addDomains   []string | ||||
| 	) | ||||
| 	httpPort, _, err := getAppInstallPort(constant.AppOpenresty) | ||||
| 	if err != nil { | ||||
|  | @ -618,7 +608,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	domainModels, addPorts, addDomains, err = getWebsiteDomains(create.Domains, httpPort, create.WebsiteID) | ||||
| 	domainModels, addPorts, _, err = getWebsiteDomains(create.Domains, httpPort, create.WebsiteID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -626,7 +616,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) | |||
| 		_ = OperateFirewallPort(nil, addPorts) | ||||
| 	}() | ||||
| 
 | ||||
| 	if err := addListenAndServerName(website, addPorts, addDomains); err != nil { | ||||
| 	if err = addListenAndServerName(website, domainModels); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
|  | @ -868,8 +858,22 @@ func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, | |||
| 	if err != nil { | ||||
| 		return response.WebsiteHTTPS{}, err | ||||
| 	} | ||||
| 	var res response.WebsiteHTTPS | ||||
| 	res.HttpsPort = website.HttpsPort | ||||
| 	var ( | ||||
| 		res        response.WebsiteHTTPS | ||||
| 		httpsPorts []string | ||||
| 	) | ||||
| 	websiteDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteId)) | ||||
| 	for _, domain := range websiteDomains { | ||||
| 		if domain.SSL { | ||||
| 			httpsPorts = append(httpsPorts, strconv.Itoa(domain.Port)) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(httpsPorts) == 0 { | ||||
| 		nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty) | ||||
| 		res.HttpsPort = strconv.Itoa(nginxInstall.HttpsPort) | ||||
| 	} else { | ||||
| 		res.HttpsPort = strings.Join(httpsPorts, ",") | ||||
| 	} | ||||
| 	if website.WebsiteSSLID == 0 { | ||||
| 		res.Enable = false | ||||
| 		return res, nil | ||||
|  | @ -925,17 +929,17 @@ func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteH | |||
| 	if !req.Enable { | ||||
| 		website.Protocol = constant.ProtocolHTTP | ||||
| 		website.WebsiteSSLID = 0 | ||||
| 		httpsPort := website.HttpsPort | ||||
| 		if httpsPort == 0 { | ||||
| 			_, httpsPort, err = getAppInstallPort(constant.AppOpenresty) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} | ||||
| 		httpsPortStr := strconv.Itoa(httpsPort) | ||||
| 		if err := deleteListenAndServerName(website, []string{httpsPortStr, "[::]:" + httpsPortStr}, []string{}); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		//httpsPort := req.HttpsPort | ||||
| 		//if len(httpsPort) == 0 { | ||||
| 		//	_, httpsPort, err = getAppInstallPort(constant.AppOpenresty) | ||||
| 		//	if err != nil { | ||||
| 		//		return nil, err | ||||
| 		//	} | ||||
| 		//} | ||||
| 		//httpsPortStr := strconv.Itoa(httpsPort) | ||||
| 		//if err = deleteListenAndServerName(website, []string{httpsPortStr, "[::]:" + httpsPortStr}, []string{}); err != nil { | ||||
| 		//	return nil, err | ||||
| 		//} | ||||
| 		nginxParams := getNginxParamsFromStaticFile(dto.SSL, nil) | ||||
| 		nginxParams = append(nginxParams, | ||||
| 			dto.NginxParam{ | ||||
|  | @ -1035,18 +1039,18 @@ func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteH | |||
| 	} | ||||
| 
 | ||||
| 	website.Protocol = constant.ProtocolHTTPS | ||||
| 	if err := applySSL(&website, websiteSSL, req); err != nil { | ||||
| 	if err = applySSL(&website, websiteSSL, req); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	website.HttpConfig = req.HttpConfig | ||||
| 
 | ||||
| 	if websiteSSL.ID == 0 { | ||||
| 		if err := websiteSSLRepo.Create(ctx, &websiteSSL); err != nil { | ||||
| 		if err = websiteSSLRepo.Create(ctx, &websiteSSL); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		website.WebsiteSSLID = websiteSSL.ID | ||||
| 	} | ||||
| 	if err := websiteRepo.Save(ctx, &website); err != nil { | ||||
| 	if err = websiteRepo.Save(ctx, &website); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &res, nil | ||||
|  |  | |||
|  | @ -7,7 +7,6 @@ import ( | |||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | @ -35,40 +34,6 @@ import ( | |||
| 	"gorm.io/gorm" | ||||
| ) | ||||
| 
 | ||||
| func getDomain(domainStr string, defaultPort int) (model.WebsiteDomain, error) { | ||||
| 	var ( | ||||
| 		err    error | ||||
| 		domain = model.WebsiteDomain{} | ||||
| 		portN  int | ||||
| 	) | ||||
| 	domainArray := strings.Split(domainStr, ":") | ||||
| 	if len(domainArray) == 1 { | ||||
| 		domain.Domain, err = handleChineseDomain(domainArray[0]) | ||||
| 		if err != nil { | ||||
| 			return domain, err | ||||
| 		} | ||||
| 		domain.Port = defaultPort | ||||
| 		return domain, nil | ||||
| 	} | ||||
| 	if len(domainArray) > 1 { | ||||
| 		domain.Domain, err = handleChineseDomain(domainArray[0]) | ||||
| 		if err != nil { | ||||
| 			return domain, err | ||||
| 		} | ||||
| 		portStr := domainArray[1] | ||||
| 		portN, err = strconv.Atoi(portStr) | ||||
| 		if err != nil { | ||||
| 			return domain, buserr.WithName("ErrTypePort", portStr) | ||||
| 		} | ||||
| 		if portN <= 0 || portN > 65535 { | ||||
| 			return domain, buserr.New("ErrTypePortRange") | ||||
| 		} | ||||
| 		domain.Port = portN | ||||
| 		return domain, nil | ||||
| 	} | ||||
| 	return domain, nil | ||||
| } | ||||
| 
 | ||||
| func handleChineseDomain(domain string) (string, error) { | ||||
| 	if common.ContainsChinese(domain) { | ||||
| 		return common.PunycodeEncode(domain) | ||||
|  | @ -481,7 +446,7 @@ func delWafConfig(website model.Website, force bool) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func addListenAndServerName(website model.Website, ports []int, domains []string) error { | ||||
| func addListenAndServerName(website model.Website, domains []model.WebsiteDomain) error { | ||||
| 	nginxFull, err := getNginxFull(&website) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
|  | @ -489,16 +454,20 @@ func addListenAndServerName(website model.Website, ports []int, domains []string | |||
| 	nginxConfig := nginxFull.SiteConfig | ||||
| 	config := nginxFull.SiteConfig.Config | ||||
| 	server := config.FindServers()[0] | ||||
| 	for _, port := range ports { | ||||
| 		server.AddListen(strconv.Itoa(port), false) | ||||
| 		if website.IPV6 { | ||||
| 			server.UpdateListen("[::]:"+strconv.Itoa(port), false) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, domain := range domains { | ||||
| 		server.AddServerName(domain) | ||||
| 		var params []string | ||||
| 		if website.Protocol == constant.ProtocolHTTPS && domain.SSL { | ||||
| 			params = append(params, "ssl", "http2") | ||||
| 		} | ||||
| 		server.AddListen(strconv.Itoa(domain.Port), false, params...) | ||||
| 		if website.IPV6 { | ||||
| 			server.UpdateListen("[::]:"+strconv.Itoa(domain.Port), false, params...) | ||||
| 		} | ||||
| 		server.UpdateServerName([]string{domain.Domain}) | ||||
| 	} | ||||
| 	if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { | ||||
| 
 | ||||
| 	if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxFull.Install.ContainerName) | ||||
|  | @ -568,6 +537,24 @@ func createPemFile(website model.Website, websiteSSL model.WebsiteSSL) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func getHttpsPort(website *model.Website) ([]int, error) { | ||||
| 	websiteDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID)) | ||||
| 	var httpsPorts []int | ||||
| 	for _, domain := range websiteDomains { | ||||
| 		if domain.SSL { | ||||
| 			httpsPorts = append(httpsPorts, domain.Port) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(httpsPorts) == 0 { | ||||
| 		nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		httpsPorts = append(httpsPorts, nginxInstall.HttpsPort) | ||||
| 	} | ||||
| 	return httpsPorts, nil | ||||
| } | ||||
| 
 | ||||
| func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.WebsiteHTTPSOp) error { | ||||
| 	nginxFull, err := getNginxFull(website) | ||||
| 	if err != nil { | ||||
|  | @ -587,17 +574,18 @@ func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.W | |||
| 	server := config.FindServers()[0] | ||||
| 
 | ||||
| 	httpPort := strconv.Itoa(nginxFull.Install.HttpPort) | ||||
| 	httpsPort := nginxFull.Install.HttpsPort | ||||
| 	if req.HttpsPort > 0 { | ||||
| 		httpsPort = req.HttpsPort | ||||
| 	httpsPort, err := getHttpsPort(website) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	website.HttpsPort = httpsPort | ||||
| 	httpPortIPV6 := "[::]:" + httpPort | ||||
| 	httpsPortIPV6 := "[::]:" + strconv.Itoa(httpsPort) | ||||
| 
 | ||||
| 	server.UpdateListen(strconv.Itoa(httpsPort), website.DefaultServer, "ssl", "http2") | ||||
| 	if website.IPV6 { | ||||
| 		server.UpdateListen(httpsPortIPV6, website.DefaultServer, "ssl", "http2") | ||||
| 	for _, port := range httpsPort { | ||||
| 		httpsPortIPV6 := "[::]:" + strconv.Itoa(port) | ||||
| 		server.UpdateListen(strconv.Itoa(port), website.DefaultServer, "ssl", "http2") | ||||
| 		if website.IPV6 { | ||||
| 			server.UpdateListen(httpsPortIPV6, website.DefaultServer, "ssl", "http2") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	switch req.HttpConfig { | ||||
|  | @ -911,30 +899,36 @@ func changeServiceName(newComposeContent, newServiceName string) (composeByte [] | |||
| 	return yaml.Marshal(composeMap) | ||||
| } | ||||
| 
 | ||||
| func getWebsiteDomains(domains string, defaultPort int, websiteID uint) (domainModels []model.WebsiteDomain, addPorts []int, addDomains []string, err error) { | ||||
| func getWebsiteDomains(domains []request.WebsiteDomain, defaultPort int, websiteID uint) (domainModels []model.WebsiteDomain, addPorts []int, addDomains []string, err error) { | ||||
| 	var ( | ||||
| 		ports = make(map[int]struct{}) | ||||
| 		ports     = make(map[int]struct{}) | ||||
| 		existPort = make(map[int]struct{}) | ||||
| 	) | ||||
| 	domainArray := strings.Split(domains, "\n") | ||||
| 	for _, domain := range domainArray { | ||||
| 		if domain == "" { | ||||
| 	existDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID)) | ||||
| 	for _, domain := range existDomains { | ||||
| 		existPort[domain.Port] = struct{}{} | ||||
| 	} | ||||
| 	for _, domain := range domains { | ||||
| 		if domain.Domain == "" { | ||||
| 			continue | ||||
| 		} | ||||
| 		if !common.IsValidDomain(domain) { | ||||
| 			err = buserr.WithName("ErrDomainFormat", domain) | ||||
| 		if !common.IsValidDomain(domain.Domain) { | ||||
| 			err = buserr.WithName("ErrDomainFormat", domain.Domain) | ||||
| 			return | ||||
| 		} | ||||
| 		var domainModel model.WebsiteDomain | ||||
| 		domainModel, err = getDomain(domain, defaultPort) | ||||
| 		domainModel.Domain, err = handleChineseDomain(domain.Domain) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		if reflect.DeepEqual(domainModel, model.WebsiteDomain{}) { | ||||
| 			continue | ||||
| 		domainModel.Port = domain.Port | ||||
| 		if domain.Port == 0 { | ||||
| 			domain.Port = defaultPort | ||||
| 		} | ||||
| 		domainModel.SSL = domain.SSL | ||||
| 		domainModel.WebsiteID = websiteID | ||||
| 		domainModels = append(domainModels, domainModel) | ||||
| 		if domainModel.Port != defaultPort { | ||||
| 		if _, ok := existPort[domainModel.Port]; !ok { | ||||
| 			ports[domainModel.Port] = struct{}{} | ||||
| 		} | ||||
| 		if exist, _ := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domainModel.Domain), websiteDomainRepo.WithWebsiteId(websiteID)); exist.ID == 0 { | ||||
|  | @ -950,6 +944,10 @@ func getWebsiteDomains(domains string, defaultPort int, websiteID uint) (domainM | |||
| 	} | ||||
| 
 | ||||
| 	for port := range ports { | ||||
| 		if port == defaultPort { | ||||
| 			addPorts = append(addPorts, port) | ||||
| 			continue | ||||
| 		} | ||||
| 		if existPorts, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithPort(port)); len(existPorts) == 0 { | ||||
| 			errMap := make(map[string]interface{}) | ||||
| 			errMap["port"] = port | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ func Init() { | |||
| 		migrations.InitPHPExtensions, | ||||
| 		migrations.AddTask, | ||||
| 		migrations.UpdateWebsite, | ||||
| 		migrations.UpdateWebsiteDomain, | ||||
| 	}) | ||||
| 	if err := m.Migrate(); err != nil { | ||||
| 		global.LOG.Error(err) | ||||
|  |  | |||
|  | @ -296,3 +296,11 @@ var UpdateWebsite = &gormigrate.Migration{ | |||
| 			&model.Website{}) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| var UpdateWebsiteDomain = &gormigrate.Migration{ | ||||
| 	ID: "20240808-update-website-domain", | ||||
| 	Migrate: func(tx *gorm.DB) error { | ||||
| 		return tx.AutoMigrate( | ||||
| 			&model.WebsiteDomain{}) | ||||
| 	}, | ||||
| } | ||||
|  |  | |||
|  | @ -67,14 +67,12 @@ export namespace Website { | |||
|     } | ||||
| 
 | ||||
|     export interface WebSiteCreateReq { | ||||
|         primaryDomain: string; | ||||
|         type: string; | ||||
|         alias: string; | ||||
|         remark: string; | ||||
|         appType: string; | ||||
|         appInstallId: number; | ||||
|         webSiteGroupId: number; | ||||
|         otherDomains: string; | ||||
|         proxy: string; | ||||
|         proxyType: string; | ||||
|         ftpUser: string; | ||||
|  | @ -88,6 +86,7 @@ export namespace Website { | |||
|         dbFormat?: string; | ||||
|         dbUser?: string; | ||||
|         dbHost?: string; | ||||
|         domains: SubDomain[]; | ||||
|     } | ||||
| 
 | ||||
|     export interface WebSiteUpdateReq { | ||||
|  | @ -128,7 +127,13 @@ export namespace Website { | |||
| 
 | ||||
|     export interface DomainCreate { | ||||
|         websiteID: number; | ||||
|         domains: string; | ||||
|         domains: SubDomain[]; | ||||
|     } | ||||
| 
 | ||||
|     interface SubDomain { | ||||
|         domain: string; | ||||
|         port: number; | ||||
|         ssl: boolean; | ||||
|     } | ||||
| 
 | ||||
|     export interface DomainDelete { | ||||
|  | @ -288,7 +293,7 @@ export namespace Website { | |||
|         SSLProtocol: string[]; | ||||
|         algorithm: string; | ||||
|         hsts: boolean; | ||||
|         httpsPort: number; | ||||
|         httpsPort?: string; | ||||
|     } | ||||
| 
 | ||||
|     export interface CheckReq { | ||||
|  |  | |||
|  | @ -2130,6 +2130,8 @@ const message = { | |||
|             "When the reverse proxy backend is HTTPS, you might need to set the origin SNI. Please refer to the CDN service provider's documentation for details.", | ||||
|         createDb: 'Create Database', | ||||
|         enableSSLHelper: 'Failure to enable will not affect the creation of the website', | ||||
|         batchAdd: 'Batch Add Domains', | ||||
|         generateDomain: 'Generate', | ||||
|     }, | ||||
|     php: { | ||||
|         short_open_tag: 'Short tag support', | ||||
|  |  | |||
|  | @ -1980,6 +1980,8 @@ const message = { | |||
|         sniHelper: '反代後端為 https 的時候可能需要設置回源 SNI,具體需要看 CDN 服務商文檔', | ||||
|         createDb: '建立資料庫', | ||||
|         enableSSLHelper: '開啟失敗不會影響網站創建', | ||||
|         batchAdd: '批量添加域名', | ||||
|         generateDomain: '生成', | ||||
|     }, | ||||
|     php: { | ||||
|         short_open_tag: '短標簽支持', | ||||
|  |  | |||
|  | @ -1982,6 +1982,8 @@ const message = { | |||
|         sniHelper: '反代后端为 https 的时候可能需要设置回源 SNI,具体需要看 CDN 服务商文档', | ||||
|         createDb: '创建数据库', | ||||
|         enableSSLHelper: '开启失败不会影响网站创建', | ||||
|         batchAdd: '批量添加域名', | ||||
|         generateDomain: '生成', | ||||
|     }, | ||||
|     php: { | ||||
|         short_open_tag: '短标签支持', | ||||
|  |  | |||
|  | @ -1,30 +1,8 @@ | |||
| <template> | ||||
|     <el-drawer | ||||
|         v-model="open" | ||||
|         :close-on-click-modal="false" | ||||
|         :close-on-press-escape="false" | ||||
|         :title="$t('website.addDomain')" | ||||
|         size="40%" | ||||
|         :before-close="handleClose" | ||||
|     > | ||||
|         <template #header> | ||||
|             <DrawerHeader :header="$t('website.addDomain')" :back="handleClose" /> | ||||
|         </template> | ||||
| 
 | ||||
|         <el-row v-loading="loading"> | ||||
|             <el-col :span="22" :offset="1"> | ||||
|                 <el-form ref="domainForm" label-position="top" :model="domain" :rules="rules"> | ||||
|                     <el-form-item :label="$t('website.domain')" prop="domains"> | ||||
|                         <el-input | ||||
|                             type="textarea" | ||||
|                             :rows="3" | ||||
|                             v-model="domain.domains" | ||||
|                             :placeholder="$t('website.domainHelper')" | ||||
|                         ></el-input> | ||||
|                     </el-form-item> | ||||
|                 </el-form> | ||||
|             </el-col> | ||||
|         </el-row> | ||||
|     <DrawerPro v-model="open" :header="$t('website.addDomain')" :back="handleClose"> | ||||
|         <el-form ref="domainForm" label-position="top" :model="create"> | ||||
|             <DomainCreate v-model:form="create"></DomainCreate> | ||||
|         </el-form> | ||||
|         <template #footer> | ||||
|             <span class="dialog-footer"> | ||||
|                 <el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button> | ||||
|  | @ -33,29 +11,31 @@ | |||
|                 </el-button> | ||||
|             </span> | ||||
|         </template> | ||||
|     </el-drawer> | ||||
|     </DrawerPro> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import DrawerHeader from '@/components/drawer-header/index.vue'; | ||||
| import { CreateDomain } from '@/api/modules/website'; | ||||
| import { Rules } from '@/global/form-rules'; | ||||
| import i18n from '@/lang'; | ||||
| import { FormInstance } from 'element-plus'; | ||||
| import { ref } from 'vue'; | ||||
| import { MsgSuccess } from '@/utils/message'; | ||||
| import DomainCreate from '@/views/website/website/domain-create/index.vue'; | ||||
| 
 | ||||
| const domainForm = ref<FormInstance>(); | ||||
| 
 | ||||
| const rules = ref({ | ||||
|     domains: [Rules.requiredInput], | ||||
| const initDomain = () => ({ | ||||
|     domain: '', | ||||
|     port: 80, | ||||
|     ssl: false, | ||||
| }); | ||||
| 
 | ||||
| const open = ref(false); | ||||
| const loading = ref(false); | ||||
| const domain = ref({ | ||||
| const create = ref({ | ||||
|     websiteID: 0, | ||||
|     domains: '', | ||||
|     domains: [initDomain()], | ||||
|     domainStr: '', | ||||
| }); | ||||
| 
 | ||||
| const em = defineEmits(['close']); | ||||
|  | @ -66,7 +46,9 @@ const handleClose = () => { | |||
| }; | ||||
| 
 | ||||
| const acceptParams = async (websiteId: number) => { | ||||
|     domain.value.websiteID = Number(websiteId); | ||||
|     create.value.websiteID = Number(websiteId); | ||||
|     create.value.domains = [initDomain()]; | ||||
|     create.value.domainStr = ''; | ||||
|     open.value = true; | ||||
| }; | ||||
| 
 | ||||
|  | @ -77,7 +59,7 @@ const submit = async (formEl: FormInstance | undefined) => { | |||
|             return; | ||||
|         } | ||||
|         loading.value = true; | ||||
|         CreateDomain(domain.value) | ||||
|         CreateDomain(create.value) | ||||
|             .then(() => { | ||||
|                 MsgSuccess(i18n.global.t('commons.msg.createSuccess')); | ||||
|                 handleClose(); | ||||
|  |  | |||
|  | @ -13,8 +13,8 @@ | |||
|                     <el-switch v-model="form.enable" @change="changeEnable"></el-switch> | ||||
|                 </el-form-item> | ||||
|                 <div v-if="form.enable"> | ||||
|                     <el-form-item :label="'HTTPS ' + $t('commons.table.port')" prop="httpsPort"> | ||||
|                         <el-input v-model.number="form.httpsPort" /> | ||||
|                     <el-form-item :label="'HTTPS ' + $t('commons.table.port')" prop="HttpsPort"> | ||||
|                         <el-text>{{ form.httpsPort }}</el-text> | ||||
|                     </el-form-item> | ||||
|                     <el-text type="warning" class="!ml-2">{{ $t('website.ipWebsiteWarn') }}</el-text> | ||||
|                     <el-divider content-position="left">{{ $t('website.SSLConfig') }}</el-divider> | ||||
|  | @ -173,7 +173,7 @@ import { GetHTTPSConfig, ListSSL, SearchAcmeAccount, UpdateHTTPSConfig } from '@ | |||
| import { ElMessageBox, FormInstance } from 'element-plus'; | ||||
| import { computed, onMounted, reactive, ref } from 'vue'; | ||||
| import i18n from '@/lang'; | ||||
| import { Rules, checkNumberRange } from '@/global/form-rules'; | ||||
| import { Rules } from '@/global/form-rules'; | ||||
| import { dateFormatSimple, getProvider, getAccountName } from '@/utils/util'; | ||||
| import { MsgSuccess } from '@/utils/message'; | ||||
| import FileList from '@/components/file-list/index.vue'; | ||||
|  | @ -204,7 +204,7 @@ const form = reactive({ | |||
|     algorithm: | ||||
|         'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED', | ||||
|     SSLProtocol: ['TLSv1.3', 'TLSv1.2', 'TLSv1.1', 'TLSv1'], | ||||
|     httpsPort: 443, | ||||
|     httpsPort: '443', | ||||
| }); | ||||
| const loading = ref(false); | ||||
| const ssls = ref(); | ||||
|  | @ -222,7 +222,6 @@ const rules = ref({ | |||
|     SSLProtocol: [Rules.requiredSelect], | ||||
|     algorithm: [Rules.requiredInput], | ||||
|     acmeAccountID: [Rules.requiredInput], | ||||
|     httpsPort: [Rules.requiredInput, checkNumberRange(1, 65535)], | ||||
| }); | ||||
| const resData = ref(); | ||||
| const sslReq = reactive({ | ||||
|  | @ -301,9 +300,7 @@ const get = () => { | |||
|                 form.acmeAccountID = data.SSL.acmeAccountId; | ||||
|             } | ||||
|             form.hsts = data.hsts; | ||||
|             if (data.httpsPort > 0) { | ||||
|                 form.httpsPort = data.httpsPort; | ||||
|             } | ||||
|             form.httpsPort = data.httpsPort; | ||||
|         } | ||||
|         listSSL(); | ||||
|         listAcmeAccount(); | ||||
|  |  | |||
|  | @ -262,21 +262,7 @@ | |||
|                         <span class="input-help">{{ $t('app.allowPortHelper') }}</span> | ||||
|                     </el-form-item> | ||||
|                 </div> | ||||
|                 <el-form-item :label="$t('website.primaryDomain')" prop="primaryDomain"> | ||||
|                     <el-input | ||||
|                         v-model.trim="website.primaryDomain" | ||||
|                         @input="changeAlias(website.primaryDomain)" | ||||
|                         :placeholder="$t('website.primaryDomainHelper')" | ||||
|                     ></el-input> | ||||
|                 </el-form-item> | ||||
|                 <el-form-item :label="$t('website.otherDomains')" prop="otherDomains"> | ||||
|                     <el-input | ||||
|                         type="textarea" | ||||
|                         :rows="3" | ||||
|                         v-model="website.otherDomains" | ||||
|                         :placeholder="$t('website.domainHelper')" | ||||
|                     ></el-input> | ||||
|                 </el-form-item> | ||||
|                 <DomainCreate v-model:form="website"></DomainCreate> | ||||
|                 <el-form-item prop="IPV6"> | ||||
|                     <el-checkbox v-model="website.IPV6" :label="$t('website.ipv6')" size="large" /> | ||||
|                 </el-form-item> | ||||
|  | @ -517,7 +503,7 @@ import { ElForm, FormInstance } from 'element-plus'; | |||
| import { reactive, ref } from 'vue'; | ||||
| import Params from '@/views/app-store/detail/params/index.vue'; | ||||
| import Check from '../check/index.vue'; | ||||
| import { MsgError, MsgSuccess } from '@/utils/message'; | ||||
| import { MsgSuccess } from '@/utils/message'; | ||||
| import { GetGroupList } from '@/api/modules/group'; | ||||
| import { Group } from '@/api/interface/group'; | ||||
| import { SearchRuntimes } from '@/api/modules/runtime'; | ||||
|  | @ -528,6 +514,7 @@ import { GetAppService } from '@/api/modules/app'; | |||
| import { v4 as uuidv4 } from 'uuid'; | ||||
| import { dateFormatSimple, getProvider, getAccountName } from '@/utils/util'; | ||||
| import { Website } from '@/api/interface/website'; | ||||
| import DomainCreate from '@/views/website/website/domain-create/index.vue'; | ||||
| 
 | ||||
| const websiteForm = ref<FormInstance>(); | ||||
| 
 | ||||
|  | @ -577,6 +564,7 @@ const initData = () => ({ | |||
|     enableSSL: false, | ||||
|     websiteSSLID: undefined, | ||||
|     acmeAccountID: undefined, | ||||
|     domains: [], | ||||
| }); | ||||
| const website = ref(initData()); | ||||
| const rules = ref<any>({ | ||||
|  | @ -813,16 +801,6 @@ const changeAppType = (type: string) => { | |||
|     } | ||||
| }; | ||||
| 
 | ||||
| function isSubsetOfStrArray(primaryDomain: string, otherDomains: string): boolean { | ||||
|     const arr: string[] = otherDomains.split('\n'); | ||||
|     for (const item of arr) { | ||||
|         if (primaryDomain === item) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| const openTaskLog = (taskID: string) => { | ||||
|     taskLog.value.acceptParams(taskID); | ||||
| }; | ||||
|  | @ -859,12 +837,6 @@ const submit = async (formEl: FormInstance | undefined) => { | |||
|             return; | ||||
|         } | ||||
|         loading.value = true; | ||||
|         const flag = isSubsetOfStrArray(website.value.primaryDomain, website.value.otherDomains); | ||||
|         if (!flag) { | ||||
|             MsgError(i18n.global.t('website.containWarn')); | ||||
|             loading.value = false; | ||||
|             return; | ||||
|         } | ||||
|         PreCheck({}) | ||||
|             .then((res) => { | ||||
|                 if (res.data) { | ||||
|  | @ -897,6 +869,17 @@ const submit = async (formEl: FormInstance | undefined) => { | |||
|     }); | ||||
| }; | ||||
| 
 | ||||
| watch( | ||||
|     () => website.value.domains, | ||||
|     (value) => { | ||||
|         if (value.length > 0) { | ||||
|             const firstDomain = value[0].domain; | ||||
|             changeAlias(firstDomain); | ||||
|         } | ||||
|     }, | ||||
|     { deep: true }, | ||||
| ); | ||||
| 
 | ||||
| const changeAlias = (value: string) => { | ||||
|     const domain = value.split(':')[0]; | ||||
|     website.value.alias = domain; | ||||
|  |  | |||
							
								
								
									
										138
									
								
								frontend/src/views/website/website/domain-create/index.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								frontend/src/views/website/website/domain-create/index.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,138 @@ | |||
| <template> | ||||
|     <div> | ||||
|         <el-form-item :label="$t('website.batchAdd')"> | ||||
|             <el-row :gutter="20"> | ||||
|                 <el-col :span="20"> | ||||
|                     <el-input | ||||
|                         class="p-w-400" | ||||
|                         type="textarea" | ||||
|                         :rows="3" | ||||
|                         v-model="create.domainStr" | ||||
|                         :placeholder="$t('website.domainHelper')" | ||||
|                     ></el-input> | ||||
|                 </el-col> | ||||
|                 <el-col :span="4"> | ||||
|                     <el-button @click="gengerateDomains" :disabled="create.domainStr == ''"> | ||||
|                         {{ $t('website.generateDomain') }} | ||||
|                     </el-button> | ||||
|                 </el-col> | ||||
|             </el-row> | ||||
|         </el-form-item> | ||||
|         <el-row :gutter="20" v-for="(domain, index) of create.domains" :key="index"> | ||||
|             <el-col :span="8"> | ||||
|                 <el-form-item | ||||
|                     :label="index == 0 ? $t('website.domain') : ''" | ||||
|                     :prop="`domains.${index}.domain`" | ||||
|                     :rules="rules.domain" | ||||
|                 > | ||||
|                     <el-input | ||||
|                         type="string" | ||||
|                         v-model="create.domains[index].domain" | ||||
|                         :placeholder="index > 0 ? $t('website.domain') : ''" | ||||
|                     ></el-input> | ||||
|                 </el-form-item> | ||||
|             </el-col> | ||||
|             <el-col :span="8"> | ||||
|                 <el-form-item | ||||
|                     :label="index == 0 ? $t('commons.table.port') : ''" | ||||
|                     :prop="`domains.${index}.port`" | ||||
|                     :rules="rules.port" | ||||
|                 > | ||||
|                     <el-input type="number" v-model.number="create.domains[index].port"></el-input> | ||||
|                 </el-form-item> | ||||
|             </el-col> | ||||
|             <el-col :span="4"> | ||||
|                 <el-form-item :label="index == 0 ? 'SSL' : ''" prop="ssl"> | ||||
|                     <el-checkbox | ||||
|                         v-model="create.domains[index].ssl" | ||||
|                         :disabled="create.domains[index].port == 80" | ||||
|                     ></el-checkbox> | ||||
|                 </el-form-item> | ||||
|             </el-col> | ||||
|             <el-col :span="4" v-if="index == 0"> | ||||
|                 <el-form-item :label="$t('commons.button.add') + $t('commons.table.port')"> | ||||
|                     <el-button @click="addDomain"> | ||||
|                         <el-icon><Plus /></el-icon> | ||||
|                     </el-button> | ||||
|                 </el-form-item> | ||||
|             </el-col> | ||||
|             <el-col :span="4" v-else> | ||||
|                 <el-form-item> | ||||
|                     <el-button @click="removeDomain(index)" link type="primary"> | ||||
|                         <el-icon><Delete /></el-icon> | ||||
|                     </el-button> | ||||
|                 </el-form-item> | ||||
|             </el-col> | ||||
|         </el-row> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { Rules, checkNumberRange } from '@/global/form-rules'; | ||||
| import { ref } from 'vue'; | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|     form: { | ||||
|         type: Object, | ||||
|         default: function () { | ||||
|             return {}; | ||||
|         }, | ||||
|     }, | ||||
| }); | ||||
| const rules = ref({ | ||||
|     port: [Rules.requiredInput, Rules.paramPort, checkNumberRange(1, 65535)], | ||||
|     domain: [Rules.requiredInput, Rules.domain], | ||||
|     domains: { | ||||
|         type: Array, | ||||
|     }, | ||||
| }); | ||||
| const initDomain = () => ({ | ||||
|     domain: '', | ||||
|     port: 80, | ||||
|     ssl: false, | ||||
| }); | ||||
| const create = ref({ | ||||
|     websiteID: 0, | ||||
|     domains: [initDomain()], | ||||
|     domainStr: '', | ||||
| }); | ||||
| 
 | ||||
| const addDomain = () => { | ||||
|     create.value.domains.push(initDomain()); | ||||
| }; | ||||
| 
 | ||||
| const removeDomain = (index: number) => { | ||||
|     create.value.domains.splice(index, 1); | ||||
| }; | ||||
| 
 | ||||
| const gengerateDomains = () => { | ||||
|     const lines = create.value.domainStr.split(/\r?\n/); | ||||
|     lines.forEach((line) => { | ||||
|         const [domain, port] = line.split(':'); | ||||
|         const exists = (domain: string, port: number): boolean => { | ||||
|             return create.value.domains.some((info) => info.domain === domain && info.port === port); | ||||
|         }; | ||||
|         if (exists(domain, port ? Number(port) : 80)) { | ||||
|             return; | ||||
|         } | ||||
|         if (create.value.domains[0].domain == '') { | ||||
|             create.value.domains[0].domain = domain; | ||||
|             create.value.domains[0].port = port ? Number(port) : 80; | ||||
|         } else { | ||||
|             create.value.domains.push({ | ||||
|                 domain, | ||||
|                 port: port ? Number(port) : 80, | ||||
|                 ssl: false, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| const handleParams = () => { | ||||
|     props.form.domains = create.value.domains; | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
|     handleParams(); | ||||
| }); | ||||
| </script> | ||||
		Loading…
	
	Add table
		
		Reference in a new issue