129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
package sitesvc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/panelhosting/panel/internal/auth"
|
|
"github.com/panelhosting/panel/internal/models"
|
|
"github.com/panelhosting/panel/internal/repository"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidSiteName = errors.New("site name must be 3-64 chars: lowercase letters, digits, hyphen")
|
|
ErrInvalidDomain = errors.New("invalid domain")
|
|
ErrInvalidPHPVersion = errors.New("unsupported php version")
|
|
ErrInvalidSSLEmail = errors.New("invalid ssl email")
|
|
ErrSSLEmailRequired = errors.New("ssl email is required when issuing certificate")
|
|
)
|
|
|
|
var (
|
|
siteNameRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$`)
|
|
domainRegex = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`)
|
|
)
|
|
|
|
type SSLIssuer interface {
|
|
IssueAsync(sslID, domainID int64, domain, webroot, email string)
|
|
IssueForSite(ctx context.Context, siteID int64, email string) error
|
|
}
|
|
|
|
type Service struct {
|
|
sites *repository.SiteRepository
|
|
servers *repository.ServerRepository
|
|
phpVers *repository.PHPVersionRepository
|
|
ssl SSLIssuer
|
|
}
|
|
|
|
func NewService(sites *repository.SiteRepository, servers *repository.ServerRepository, php *repository.PHPVersionRepository, ssl SSLIssuer) *Service {
|
|
return &Service{sites: sites, servers: servers, phpVers: php, ssl: ssl}
|
|
}
|
|
|
|
type CreateInput struct {
|
|
Name string
|
|
Domain string
|
|
PHPVersion string
|
|
SSLEmail string
|
|
ServerID int64
|
|
OwnerID int64
|
|
IsAdmin bool
|
|
IssueSSL bool
|
|
}
|
|
|
|
func (s *Service) List(ctx context.Context, user *models.User, isAdmin bool) ([]repository.SiteWithDomain, error) {
|
|
return s.sites.ListForUser(ctx, user.ID, isAdmin)
|
|
}
|
|
|
|
func (s *Service) ListPHPVersions(ctx context.Context) ([]string, error) {
|
|
return s.phpVers.ListActive(ctx)
|
|
}
|
|
|
|
func (s *Service) Create(ctx context.Context, in CreateInput) (*repository.SiteWithDomain, error) {
|
|
name := strings.ToLower(strings.TrimSpace(in.Name))
|
|
domain := strings.ToLower(strings.TrimSpace(in.Domain))
|
|
phpVersion := strings.TrimSpace(in.PHPVersion)
|
|
sslEmail := strings.TrimSpace(in.SSLEmail)
|
|
|
|
if !siteNameRegex.MatchString(name) {
|
|
return nil, ErrInvalidSiteName
|
|
}
|
|
if !domainRegex.MatchString(domain) {
|
|
return nil, ErrInvalidDomain
|
|
}
|
|
if phpVersion == "" {
|
|
return nil, ErrInvalidPHPVersion
|
|
}
|
|
if in.IssueSSL {
|
|
if sslEmail == "" {
|
|
return nil, ErrSSLEmailRequired
|
|
}
|
|
if err := auth.ValidateEmail(sslEmail); err != nil {
|
|
return nil, ErrInvalidSSLEmail
|
|
}
|
|
}
|
|
|
|
active, err := s.phpVers.IsActive(ctx, phpVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !active {
|
|
return nil, ErrInvalidPHPVersion
|
|
}
|
|
|
|
serverID := in.ServerID
|
|
if serverID == 0 {
|
|
srv, err := s.servers.GetFirst(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("no server available: %w", err)
|
|
}
|
|
serverID = srv.ID
|
|
} else {
|
|
if _, err := s.servers.GetByID(ctx, serverID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
documentRoot := fmt.Sprintf("/var/www/%s/public", name)
|
|
site, err := s.sites.Create(ctx, serverID, in.OwnerID, name, documentRoot, phpVersion, domain, sslEmail, in.IssueSSL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if in.IssueSSL && s.ssl != nil && site.SSLID > 0 {
|
|
s.ssl.IssueAsync(site.SSLID, site.DomainID, domain, documentRoot, sslEmail)
|
|
}
|
|
|
|
return site, nil
|
|
}
|
|
|
|
func (s *Service) ReissueSSLAsync(siteID int64, email string) {
|
|
if s.ssl == nil {
|
|
return
|
|
}
|
|
go func() {
|
|
_ = s.ssl.IssueForSite(context.Background(), siteID, email)
|
|
}()
|
|
}
|