Files

201 lines
5.7 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/panelhosting/panel/internal/models"
"github.com/panelhosting/panel/internal/phpversions"
)
type SiteRepository struct {
pool *pgxpool.Pool
}
func NewSiteRepository(pool *pgxpool.Pool) *SiteRepository {
return &SiteRepository{pool: pool}
}
type SiteWithDomain struct {
models.Site
PrimaryDomain string `json:"primary_domain"`
DomainID int64 `json:"domain_id,omitempty"`
SSLID int64 `json:"ssl_id,omitempty"`
SSLStatus string `json:"ssl_status,omitempty"`
SSLError string `json:"ssl_error,omitempty"`
SSLEmail string `json:"ssl_email,omitempty"`
}
func (r *SiteRepository) Create(ctx context.Context, serverID, ownerID int64, name, documentRoot, phpVersion, domain, sslEmail string, issueSSL bool) (*SiteWithDomain, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
const siteQ = `
INSERT INTO sites (server_id, owner_id, name, document_root, php_version, ssl_email, status)
VALUES ($1, $2, $3, $4, $5, NULLIF($6, ''), 'active')
RETURNING id, uuid, server_id, owner_id, name, document_root, php_version, status,
settings, disk_quota_mb, created_at, updated_at
`
var s models.Site
var phpVer *string
if phpVersion != "" {
phpVer = &phpVersion
}
err = tx.QueryRow(ctx, siteQ, serverID, ownerID, name, documentRoot, phpVer, sslEmail).Scan(
&s.ID, &s.UUID, &s.ServerID, &s.OwnerID, &s.Name, &s.DocumentRoot, &s.PHPVersion, &s.Status,
&s.Settings, &s.DiskQuotaMB, &s.CreatedAt, &s.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("create site: %w", err)
}
const domainQ = `
INSERT INTO domains (site_id, domain, is_primary, ssl_enabled)
VALUES ($1, $2, true, $3)
RETURNING id
`
var domainID int64
if err = tx.QueryRow(ctx, domainQ, s.ID, domain, issueSSL).Scan(&domainID); err != nil {
return nil, fmt.Errorf("create domain: %w", err)
}
result := &SiteWithDomain{Site: s, PrimaryDomain: domain, DomainID: domainID, SSLEmail: sslEmail}
if issueSSL {
const sslQ = `
INSERT INTO ssl_certificates (domain_id, type, status, auto_renew)
VALUES ($1, 'letsencrypt', 'pending', true)
RETURNING id
`
if err = tx.QueryRow(ctx, sslQ, domainID).Scan(&result.SSLID); err != nil {
return nil, fmt.Errorf("create ssl cert: %w", err)
}
result.SSLStatus = "pending"
}
if err = tx.Commit(ctx); err != nil {
return nil, err
}
return result, nil
}
func (r *SiteRepository) ListForUser(ctx context.Context, userID int64, isAdmin bool) ([]SiteWithDomain, error) {
var q string
var args []any
if isAdmin {
q = siteListQuery + ` ORDER BY s.created_at DESC`
} else {
q = siteListQuery + ` WHERE s.owner_id = $1 ORDER BY s.created_at DESC`
args = append(args, userID)
}
rows, err := r.pool.Query(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("list sites: %w", err)
}
defer rows.Close()
return scanSiteList(rows)
}
const siteListQuery = `
SELECT s.id, s.uuid, s.server_id, s.owner_id, s.name, s.document_root, s.php_version, s.status,
s.settings, s.disk_quota_mb, s.created_at, s.updated_at,
COALESCE(d.domain::text, ''),
COALESCE(d.id, 0),
COALESCE(ssl.status::text, 'none'),
COALESCE(ssl.error_message, ''),
COALESCE(s.ssl_email::text, '')
FROM sites s
LEFT JOIN domains d ON d.site_id = s.id AND d.is_primary = true
LEFT JOIN LATERAL (
SELECT status, error_message FROM ssl_certificates
WHERE domain_id = d.id ORDER BY id DESC LIMIT 1
) ssl ON true
`
func scanSiteList(rows pgx.Rows) ([]SiteWithDomain, error) {
var list []SiteWithDomain
for rows.Next() {
var item SiteWithDomain
err := rows.Scan(
&item.ID, &item.UUID, &item.ServerID, &item.OwnerID, &item.Name, &item.DocumentRoot,
&item.PHPVersion, &item.Status, &item.Settings, &item.DiskQuotaMB,
&item.CreatedAt, &item.UpdatedAt, &item.PrimaryDomain,
&item.DomainID, &item.SSLStatus, &item.SSLError, &item.SSLEmail,
)
if err != nil {
return nil, err
}
list = append(list, item)
}
return list, rows.Err()
}
func (r *SiteRepository) UpdateSSLEmail(ctx context.Context, siteID int64, email string) error {
_, err := r.pool.Exec(ctx, `UPDATE sites SET ssl_email = $2, updated_at = now() WHERE id = $1`, siteID, email)
return err
}
func (r *SiteRepository) GetSSLEmail(ctx context.Context, siteID int64) (string, error) {
var email *string
err := r.pool.QueryRow(ctx, `SELECT ssl_email::text FROM sites WHERE id = $1`, siteID).Scan(&email)
if err != nil {
return "", err
}
if email == nil {
return "", nil
}
return *email, nil
}
type PHPVersionRepository struct {
pool *pgxpool.Pool
}
func NewPHPVersionRepository(pool *pgxpool.Pool) *PHPVersionRepository {
return &PHPVersionRepository{pool: pool}
}
func (r *PHPVersionRepository) ListActive(ctx context.Context) ([]string, error) {
rows, err := r.pool.Query(ctx, `
SELECT version FROM php_versions WHERE is_active = true ORDER BY sort_order
`)
if err != nil {
return phpversions.Defaults, nil
}
defer rows.Close()
var versions []string
for rows.Next() {
var v string
if err := rows.Scan(&v); err != nil {
return phpversions.Defaults, nil
}
versions = append(versions, v)
}
if err := rows.Err(); err != nil || len(versions) == 0 {
return phpversions.Defaults, nil
}
return versions, nil
}
func (r *PHPVersionRepository) IsActive(ctx context.Context, version string) (bool, error) {
var active bool
err := r.pool.QueryRow(ctx, `SELECT is_active FROM php_versions WHERE version = $1`, version).Scan(&active)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return phpversions.Contains(version), nil
}
return phpversions.Contains(version), nil
}
return active, nil
}