feat: Let's Encrypt SSL on site creation

This commit is contained in:
orohi
2026-06-17 05:57:01 +03:00
parent 9b7eb99d20
commit 00d6789897
18 changed files with 703 additions and 51 deletions
+33 -5
View File
@@ -22,9 +22,13 @@ func NewSiteRepository(pool *pgxpool.Pool) *SiteRepository {
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"`
}
func (r *SiteRepository) Create(ctx context.Context, serverID, ownerID int64, name, documentRoot, phpVersion, domain string) (*SiteWithDomain, error) {
func (r *SiteRepository) Create(ctx context.Context, serverID, ownerID int64, name, documentRoot, phpVersion, domain string, issueSSL bool) (*SiteWithDomain, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
@@ -52,17 +56,33 @@ func (r *SiteRepository) Create(ctx context.Context, serverID, ownerID int64, na
const domainQ = `
INSERT INTO domains (site_id, domain, is_primary, ssl_enabled)
VALUES ($1, $2, true, true)
VALUES ($1, $2, true, $3)
RETURNING id
`
if _, err = tx.Exec(ctx, domainQ, s.ID, domain); err != nil {
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}
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 &SiteWithDomain{Site: s, PrimaryDomain: domain}, nil
return result, nil
}
func (r *SiteRepository) ListForUser(ctx context.Context, userID int64, isAdmin bool) ([]SiteWithDomain, error) {
@@ -87,9 +107,16 @@ func (r *SiteRepository) ListForUser(ctx context.Context, userID int64, isAdmin
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.domain::text, ''),
COALESCE(d.id, 0),
COALESCE(ssl.status::text, 'none'),
COALESCE(ssl.error_message, '')
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) {
@@ -100,6 +127,7 @@ func scanSiteList(rows pgx.Rows) ([]SiteWithDomain, error) {
&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,
)
if err != nil {
return nil, err
+110
View File
@@ -0,0 +1,110 @@
package repository
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type SSLCertificate struct {
ID int64 `json:"id"`
DomainID int64 `json:"domain_id"`
Type string `json:"type"`
Status string `json:"status"`
Issuer *string `json:"issuer,omitempty"`
ErrorMessage *string `json:"error_message,omitempty"`
IssuedAt *time.Time `json:"issued_at,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
AutoRenew bool `json:"auto_renew"`
}
type SSLRepository struct {
pool *pgxpool.Pool
}
func NewSSLRepository(pool *pgxpool.Pool) *SSLRepository {
return &SSLRepository{pool: pool}
}
func (r *SSLRepository) CreatePending(ctx context.Context, domainID int64) (int64, error) {
const q = `
INSERT INTO ssl_certificates (domain_id, type, status, auto_renew)
VALUES ($1, 'letsencrypt', 'pending', true)
RETURNING id
`
var id int64
err := r.pool.QueryRow(ctx, q, domainID).Scan(&id)
if err != nil {
return 0, fmt.Errorf("create ssl cert: %w", err)
}
return id, nil
}
func (r *SSLRepository) MarkActive(ctx context.Context, id int64, issuer, certPEM, keyPEM, chainPEM string, issuedAt, expiresAt time.Time) error {
const q = `
UPDATE ssl_certificates
SET status = 'active', issuer = $2, cert_pem = $3, key_pem = $4, chain_pem = $5,
issued_at = $6, expires_at = $7, error_message = NULL, updated_at = now()
WHERE id = $1
`
_, err := r.pool.Exec(ctx, q, id, issuer, certPEM, keyPEM, chainPEM, issuedAt, expiresAt)
return err
}
func (r *SSLRepository) MarkError(ctx context.Context, id int64, msg string) error {
const q = `
UPDATE ssl_certificates
SET status = 'error', error_message = $2, updated_at = now()
WHERE id = $1
`
_, err := r.pool.Exec(ctx, q, id, msg)
return err
}
func (r *SSLRepository) GetByDomainID(ctx context.Context, domainID int64) (*SSLCertificate, error) {
const q = `
SELECT id, domain_id, type::text, status::text, issuer, error_message, issued_at, expires_at, auto_renew
FROM ssl_certificates WHERE domain_id = $1 ORDER BY id DESC LIMIT 1
`
var c SSLCertificate
err := r.pool.QueryRow(ctx, q, domainID).Scan(
&c.ID, &c.DomainID, &c.Type, &c.Status, &c.Issuer, &c.ErrorMessage,
&c.IssuedAt, &c.ExpiresAt, &c.AutoRenew,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &c, nil
}
func (r *SSLRepository) GetDomainInfo(ctx context.Context, domainID int64) (domain string, webroot string, err error) {
const q = `
SELECT d.domain::text, s.document_root
FROM domains d
JOIN sites s ON s.id = d.site_id
WHERE d.id = $1
`
err = r.pool.QueryRow(ctx, q, domainID).Scan(&domain, &webroot)
return
}
func (r *SSLRepository) GetSiteOwner(ctx context.Context, siteID int64) (int64, error) {
var ownerID int64
err := r.pool.QueryRow(ctx, `SELECT owner_id FROM sites WHERE id = $1`, siteID).Scan(&ownerID)
return ownerID, err
}
func (r *SSLRepository) GetDomainIDBySite(ctx context.Context, siteID int64) (int64, error) {
var id int64
err := r.pool.QueryRow(ctx, `
SELECT id FROM domains WHERE site_id = $1 AND is_primary = true LIMIT 1
`, siteID).Scan(&id)
return id, err
}