From 49f4147e4a4f8f6ab1cf33a4b3a5ea0f9488d49c Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Tue, 30 Sep 2025 16:28:24 +0200 Subject: [PATCH] Migrate from DNS to HTTP challenge Signed-off-by: Bryan Frimin --- pkg/coredata/custom_domain.go | 596 ++++++++++++++-------- pkg/coredata/custom_domain_order_field.go | 3 + 2 files changed, 396 insertions(+), 203 deletions(-) diff --git a/pkg/coredata/custom_domain.go b/pkg/coredata/custom_domain.go index d2abb604b..cf62d2167 100644 --- a/pkg/coredata/custom_domain.go +++ b/pkg/coredata/custom_domain.go @@ -30,28 +30,24 @@ import ( type ( CustomDomain struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - Domain string `db:"domain"` - VerificationStatus CustomDomainVerificationStatus `db:"verification_status"` - VerificationMethod *string `db:"verification_method"` - VerificationToken []byte `db:"-"` // Decrypted value - EncryptedVerificationToken []byte `db:"encrypted_verification_token"` - AcmeChallengeRecord *string `db:"acme_challenge_record"` - SSLCertificate *tls.Certificate `db:"-"` // Parsed certificate - SSLCertificatePEM []byte `db:"-"` // Decrypted PEM - EncryptedSSLCertificate []byte `db:"encrypted_ssl_certificate"` - SSLPrivateKeyPEM []byte `db:"-"` // Decrypted PEM - EncryptedSSLPrivateKey []byte `db:"encrypted_ssl_private_key"` - SSLCertificateChain *string `db:"ssl_certificate_chain"` - SSLStatus *CustomDomainSSLStatus `db:"ssl_status"` - SSLExpiresAt *time.Time `db:"ssl_expires_at"` - IsActive bool `db:"is_active"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - VerifiedAt *time.Time `db:"verified_at"` - LastVerificationAttempt *time.Time `db:"last_verification_attempt"` - VerificationAttempts int `db:"verification_attempts"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + Domain string `db:"domain"` + HTTPChallengeToken *string `db:"http_challenge_token"` + HTTPChallengeKeyAuth *string `db:"http_challenge_key_auth"` + HTTPChallengeURL *string `db:"http_challenge_url"` + HTTPOrderURL *string `db:"http_order_url"` + SSLCertificate *tls.Certificate `db:"-"` // Parsed certificate + SSLCertificatePEM []byte `db:"-"` // Decrypted PEM + EncryptedSSLCertificate []byte `db:"encrypted_ssl_certificate"` + SSLPrivateKeyPEM []byte `db:"-"` // Decrypted PEM + EncryptedSSLPrivateKey []byte `db:"encrypted_ssl_private_key"` + SSLCertificateChain *string `db:"ssl_certificate_chain"` + SSLStatus *CustomDomainSSLStatus `db:"ssl_status"` + SSLExpiresAt *time.Time `db:"ssl_expires_at"` + IsActive bool `db:"is_active"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } CustomDomains []*CustomDomain @@ -60,26 +56,26 @@ type ( func NewCustomDomain(orgID gid.GID, domain string) *CustomDomain { now := time.Now() return &CustomDomain{ - ID: gid.New(orgID.TenantID(), CustomDomainEntityType), - OrganizationID: orgID, - Domain: domain, - VerificationStatus: CustomDomainVerificationStatusPending, - IsActive: false, - VerificationAttempts: 0, - CreatedAt: now, - UpdatedAt: now, + ID: gid.New(orgID.TenantID(), CustomDomainEntityType), + OrganizationID: orgID, + Domain: domain, + IsActive: false, + CreatedAt: now, + UpdatedAt: now, } } -func (cd *CustomDomain) CursorKey(orderBy CustomDomainOrderField) page.CursorKey { - switch orderBy { +func (cd *CustomDomain) CursorKey(field CustomDomainOrderField) page.CursorKey { + switch field { case CustomDomainOrderFieldCreatedAt: return page.NewCursorKey(cd.ID, cd.CreatedAt) case CustomDomainOrderFieldDomain: return page.NewCursorKey(cd.ID, cd.Domain) - default: - panic(fmt.Sprintf("unsupported order by: %s", orderBy)) + case CustomDomainOrderFieldUpdatedAt: + return page.NewCursorKey(cd.ID, cd.UpdatedAt) } + + panic(fmt.Sprintf("unsupported order by: %s", field)) } func (cd *CustomDomain) LoadByID( @@ -94,10 +90,10 @@ SELECT id, organization_id, domain, - verification_status, - verification_method, - encrypted_verification_token, - acme_challenge_record, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, encrypted_ssl_certificate, encrypted_ssl_private_key, ssl_certificate_chain, @@ -105,10 +101,7 @@ SELECT ssl_expires_at, is_active, created_at, - updated_at, - verified_at, - last_verification_attempt, - verification_attempts + updated_at FROM custom_domains WHERE @@ -134,15 +127,91 @@ LIMIT 1 *cd = customDomain - // Decrypt verification token - if len(cd.EncryptedVerificationToken) > 0 { - decrypted, err := cipher.Decrypt(cd.EncryptedVerificationToken, encryptionKey) + // Decrypt SSL certificate + if len(cd.EncryptedSSLCertificate) > 0 { + decrypted, err := cipher.Decrypt(cd.EncryptedSSLCertificate, encryptionKey) if err != nil { - return fmt.Errorf("cannot decrypt verification token: %w", err) + return fmt.Errorf("cannot decrypt SSL certificate: %w", err) } - cd.VerificationToken = decrypted + cd.SSLCertificatePEM = decrypted } + // Decrypt SSL private key + if len(cd.EncryptedSSLPrivateKey) > 0 { + decrypted, err := cipher.Decrypt(cd.EncryptedSSLPrivateKey, encryptionKey) + if err != nil { + return fmt.Errorf("cannot decrypt SSL private key: %w", err) + } + cd.SSLPrivateKeyPEM = decrypted + } + + // Parse certificate and key into tls.Certificate if both are present + if len(cd.SSLCertificatePEM) > 0 && len(cd.SSLPrivateKeyPEM) > 0 { + fullCertPEM := string(cd.SSLCertificatePEM) + if cd.SSLCertificateChain != nil && *cd.SSLCertificateChain != "" { + fullCertPEM += "\n" + *cd.SSLCertificateChain + } + + tlsCert, err := tls.X509KeyPair([]byte(fullCertPEM), cd.SSLPrivateKeyPEM) + if err != nil { + return fmt.Errorf("cannot parse certificate and key: %w", err) + } + cd.SSLCertificate = &tlsCert + } + + return nil +} + +func (cd *CustomDomain) LoadByIDForUpdate( + ctx context.Context, + conn pg.Conn, + scope Scoper, + encryptionKey cipher.EncryptionKey, + domainID gid.GID, +) error { + q := ` +SELECT + id, + organization_id, + domain, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, + encrypted_ssl_certificate, + encrypted_ssl_private_key, + ssl_certificate_chain, + ssl_status, + ssl_expires_at, + is_active, + created_at, + updated_at +FROM + custom_domains +WHERE + %s + AND id = @domain_id +LIMIT 1 +FOR UPDATE +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.NamedArgs{"domain_id": domainID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query custom domain for update: %w", err) + } + + customDomain, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CustomDomain]) + if err != nil { + return fmt.Errorf("cannot collect custom domain: %w", err) + } + + *cd = customDomain + // Decrypt SSL certificate if len(cd.EncryptedSSLCertificate) > 0 { decrypted, err := cipher.Decrypt(cd.EncryptedSSLCertificate, encryptionKey) @@ -162,8 +231,7 @@ LIMIT 1 } // Parse certificate and key into tls.Certificate if both are present - if cd.SSLCertificatePEM != nil && cd.SSLPrivateKeyPEM != nil { - // Build full certificate PEM with chain if present + if len(cd.SSLCertificatePEM) > 0 && len(cd.SSLPrivateKeyPEM) > 0 { fullCertPEM := string(cd.SSLCertificatePEM) if cd.SSLCertificateChain != nil && *cd.SSLCertificateChain != "" { fullCertPEM += "\n" + *cd.SSLCertificateChain @@ -191,10 +259,10 @@ SELECT id, organization_id, domain, - verification_status, - verification_method, - encrypted_verification_token, - acme_challenge_record, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, encrypted_ssl_certificate, encrypted_ssl_private_key, ssl_certificate_chain, @@ -202,17 +270,14 @@ SELECT ssl_expires_at, is_active, created_at, - updated_at, - verified_at, - last_verification_attempt, - verification_attempts + updated_at FROM custom_domains WHERE %s - domain = @domain + AND domain = @domain LIMIT 1 - ` +` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -231,15 +296,6 @@ LIMIT 1 *cd = customDomain - // Decrypt verification token - if len(cd.EncryptedVerificationToken) > 0 { - decrypted, err := cipher.Decrypt(cd.EncryptedVerificationToken, encryptionKey) - if err != nil { - return fmt.Errorf("cannot decrypt verification token: %w", err) - } - cd.VerificationToken = decrypted - } - // Decrypt SSL certificate if len(cd.EncryptedSSLCertificate) > 0 { decrypted, err := cipher.Decrypt(cd.EncryptedSSLCertificate, encryptionKey) @@ -259,8 +315,7 @@ LIMIT 1 } // Parse certificate and key into tls.Certificate if both are present - if cd.SSLCertificatePEM != nil && cd.SSLPrivateKeyPEM != nil { - // Build full certificate PEM with chain if present + if len(cd.SSLCertificatePEM) > 0 && len(cd.SSLPrivateKeyPEM) > 0 { fullCertPEM := string(cd.SSLCertificatePEM) if cd.SSLCertificateChain != nil && *cd.SSLCertificateChain != "" { fullCertPEM += "\n" + *cd.SSLCertificateChain @@ -281,13 +336,8 @@ func (cd *CustomDomain) Insert( conn pg.Conn, scope Scoper, encryptionKey cipher.EncryptionKey, - verificationToken []byte, ) error { - encryptedToken, err := cipher.Encrypt(verificationToken, encryptionKey) - if err != nil { - return fmt.Errorf("cannot encrypt verification token: %w", err) - } - + var err error var encryptedCert []byte if len(cd.SSLCertificatePEM) > 0 { encryptedCert, err = cipher.Encrypt(cd.SSLCertificatePEM, encryptionKey) @@ -310,10 +360,10 @@ INSERT INTO custom_domains ( tenant_id, organization_id, domain, - verification_status, - verification_method, - encrypted_verification_token, - acme_challenge_record, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, encrypted_ssl_certificate, encrypted_ssl_private_key, ssl_certificate_chain, @@ -321,19 +371,16 @@ INSERT INTO custom_domains ( ssl_expires_at, is_active, created_at, - updated_at, - verified_at, - last_verification_attempt, - verification_attempts + updated_at ) VALUES ( @id, @tenant_id, @organization_id, @domain, - @verification_status, - @verification_method, - @encrypted_verification_token, - @acme_challenge_record, + @http_challenge_token, + @http_challenge_key_auth, + @http_challenge_url, + @http_order_url, @encrypted_ssl_certificate, @encrypted_ssl_private_key, @ssl_certificate_chain, @@ -341,33 +388,27 @@ INSERT INTO custom_domains ( @ssl_expires_at, @is_active, @created_at, - @updated_at, - @verified_at, - @last_verification_attempt, - @verification_attempts + @updated_at ) ` args := pgx.NamedArgs{ - "id": cd.ID, - "tenant_id": scope.GetTenantID(), - "organization_id": cd.OrganizationID, - "domain": cd.Domain, - "verification_status": cd.VerificationStatus, - "verification_method": cd.VerificationMethod, - "encrypted_verification_token": encryptedToken, - "acme_challenge_record": cd.AcmeChallengeRecord, - "encrypted_ssl_certificate": encryptedCert, - "encrypted_ssl_private_key": encryptedKey, - "ssl_certificate_chain": cd.SSLCertificateChain, - "ssl_status": cd.SSLStatus, - "ssl_expires_at": cd.SSLExpiresAt, - "is_active": cd.IsActive, - "created_at": cd.CreatedAt, - "updated_at": cd.UpdatedAt, - "verified_at": cd.VerifiedAt, - "last_verification_attempt": cd.LastVerificationAttempt, - "verification_attempts": cd.VerificationAttempts, + "id": cd.ID, + "tenant_id": scope.GetTenantID(), + "organization_id": cd.OrganizationID, + "domain": cd.Domain, + "http_challenge_token": cd.HTTPChallengeToken, + "http_challenge_key_auth": cd.HTTPChallengeKeyAuth, + "http_challenge_url": cd.HTTPChallengeURL, + "http_order_url": cd.HTTPOrderURL, + "encrypted_ssl_certificate": encryptedCert, + "encrypted_ssl_private_key": encryptedKey, + "ssl_certificate_chain": cd.SSLCertificateChain, + "ssl_status": cd.SSLStatus, + "ssl_expires_at": cd.SSLExpiresAt, + "is_active": cd.IsActive, + "created_at": cd.CreatedAt, + "updated_at": cd.UpdatedAt, } _, err = conn.Exec(ctx, q, args) @@ -375,8 +416,6 @@ INSERT INTO custom_domains ( return fmt.Errorf("cannot insert custom domain: %w", err) } - cd.VerificationToken = verificationToken - cd.EncryptedVerificationToken = encryptedToken cd.EncryptedSSLCertificate = encryptedCert cd.EncryptedSSLPrivateKey = encryptedKey @@ -389,15 +428,6 @@ func (cd *CustomDomain) Update( scope Scoper, encryptionKey cipher.EncryptionKey, ) error { - var encryptedToken []byte - if len(cd.VerificationToken) > 0 { - var err error - encryptedToken, err = cipher.Encrypt(cd.VerificationToken, encryptionKey) - if err != nil { - return fmt.Errorf("cannot encrypt verification token: %w", err) - } - } - var encryptedCert []byte if len(cd.SSLCertificatePEM) > 0 { var err error @@ -420,20 +450,17 @@ func (cd *CustomDomain) Update( UPDATE custom_domains SET - verification_status = @verification_status, - verification_method = @verification_method, - encrypted_verification_token = @encrypted_verification_token, - acme_challenge_record = @acme_challenge_record, + http_challenge_token = @http_challenge_token, + http_challenge_key_auth = @http_challenge_key_auth, + http_challenge_url = @http_challenge_url, + http_order_url = @http_order_url, encrypted_ssl_certificate = @encrypted_ssl_certificate, encrypted_ssl_private_key = @encrypted_ssl_private_key, ssl_certificate_chain = @ssl_certificate_chain, ssl_status = @ssl_status, ssl_expires_at = @ssl_expires_at, is_active = @is_active, - updated_at = @updated_at, - verified_at = @verified_at, - last_verification_attempt = @last_verification_attempt, - verification_attempts = @verification_attempts + updated_at = @updated_at WHERE %s AND id = @id @@ -442,21 +469,18 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.NamedArgs{ - "id": cd.ID, - "verification_status": cd.VerificationStatus, - "verification_method": cd.VerificationMethod, - "encrypted_verification_token": encryptedToken, - "acme_challenge_record": cd.AcmeChallengeRecord, - "encrypted_ssl_certificate": encryptedCert, - "encrypted_ssl_private_key": encryptedKey, - "ssl_certificate_chain": cd.SSLCertificateChain, - "ssl_status": cd.SSLStatus, - "ssl_expires_at": cd.SSLExpiresAt, - "is_active": cd.IsActive, - "updated_at": cd.UpdatedAt, - "verified_at": cd.VerifiedAt, - "last_verification_attempt": cd.LastVerificationAttempt, - "verification_attempts": cd.VerificationAttempts, + "id": cd.ID, + "http_challenge_token": cd.HTTPChallengeToken, + "http_challenge_key_auth": cd.HTTPChallengeKeyAuth, + "http_challenge_url": cd.HTTPChallengeURL, + "http_order_url": cd.HTTPOrderURL, + "encrypted_ssl_certificate": encryptedCert, + "encrypted_ssl_private_key": encryptedKey, + "ssl_certificate_chain": cd.SSLCertificateChain, + "ssl_status": cd.SSLStatus, + "ssl_expires_at": cd.SSLExpiresAt, + "is_active": cd.IsActive, + "updated_at": time.Now(), } maps.Copy(args, scope.SQLArguments()) @@ -465,7 +489,6 @@ WHERE return fmt.Errorf("cannot update custom domain: %w", err) } - cd.EncryptedVerificationToken = encryptedToken cd.EncryptedSSLCertificate = encryptedCert cd.EncryptedSSLPrivateKey = encryptedKey @@ -477,7 +500,14 @@ func (cd *CustomDomain) Delete( conn pg.Conn, scope Scoper, ) error { - q := `DELETE FROM custom_domains WHERE %s AND id = @id` + q := ` +DELETE FROM + custom_domains +WHERE + %s + AND id = @id +` + q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.NamedArgs{"id": cd.ID} @@ -491,11 +521,12 @@ func (cd *CustomDomain) Delete( return nil } -func (cds *CustomDomains) ListCustomDomainsByOrganization( +func (domains *CustomDomains) LoadByOrganizationID( ctx context.Context, conn pg.Conn, scope Scoper, - organizationID gid.GID, + encryptionKey cipher.EncryptionKey, + orgID gid.GID, cursor *page.Cursor[CustomDomainOrderField], ) error { q := ` @@ -503,10 +534,10 @@ SELECT id, organization_id, domain, - verification_status, - verification_method, - encrypted_verification_token, - acme_challenge_record, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, encrypted_ssl_certificate, encrypted_ssl_private_key, ssl_certificate_chain, @@ -514,21 +545,18 @@ SELECT ssl_expires_at, is_active, created_at, - updated_at, - verified_at, - last_verification_attempt, - verification_attempts + updated_at FROM custom_domains WHERE - %s + %s AND organization_id = @organization_id - AND %s - ` + %s +` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) - args := pgx.NamedArgs{"organization_id": organizationID} + args := pgx.NamedArgs{"organization_id": orgID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) @@ -537,17 +565,120 @@ WHERE return fmt.Errorf("cannot query custom domains: %w", err) } - domains, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CustomDomain]) + result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CustomDomain]) if err != nil { return fmt.Errorf("cannot collect custom domains: %w", err) } - *cds = domains + for _, cd := range result { + // Decrypt SSL certificate + if len(cd.EncryptedSSLCertificate) > 0 { + decrypted, err := cipher.Decrypt(cd.EncryptedSSLCertificate, encryptionKey) + if err != nil { + return fmt.Errorf("cannot decrypt SSL certificate: %w", err) + } + cd.SSLCertificatePEM = decrypted + } + + // Decrypt SSL private key + if len(cd.EncryptedSSLPrivateKey) > 0 { + decrypted, err := cipher.Decrypt(cd.EncryptedSSLPrivateKey, encryptionKey) + if err != nil { + return fmt.Errorf("cannot decrypt SSL private key: %w", err) + } + cd.SSLPrivateKeyPEM = decrypted + } + + // Parse certificate and key into tls.Certificate if both are present + if len(cd.SSLCertificatePEM) > 0 && len(cd.SSLPrivateKeyPEM) > 0 { + fullCertPEM := string(cd.SSLCertificatePEM) + if cd.SSLCertificateChain != nil && *cd.SSLCertificateChain != "" { + fullCertPEM += "\n" + *cd.SSLCertificateChain + } + + tlsCert, err := tls.X509KeyPair([]byte(fullCertPEM), cd.SSLPrivateKeyPEM) + if err != nil { + return fmt.Errorf("cannot parse certificate and key: %w", err) + } + cd.SSLCertificate = &tlsCert + } + } + + *domains = result + return nil +} + +func (cd *CustomDomain) LoadByHTTPChallengeToken( + ctx context.Context, + conn pg.Conn, + scope Scoper, + encryptionKey cipher.EncryptionKey, + token string, +) error { + q := ` +SELECT + id, + organization_id, + domain, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, + encrypted_ssl_certificate, + encrypted_ssl_private_key, + ssl_certificate_chain, + ssl_status, + ssl_expires_at, + is_active, + created_at, + updated_at +FROM + custom_domains +WHERE + %s + AND http_challenge_token = @token +LIMIT 1 +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.NamedArgs{"token": token} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query custom domain: %w", err) + } + + customDomain, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CustomDomain]) + if err != nil { + return fmt.Errorf("cannot collect custom domain: %w", err) + } + + *cd = customDomain + + // Decrypt SSL certificate + if len(cd.EncryptedSSLCertificate) > 0 { + decrypted, err := cipher.Decrypt(cd.EncryptedSSLCertificate, encryptionKey) + if err != nil { + return fmt.Errorf("cannot decrypt SSL certificate: %w", err) + } + cd.SSLCertificatePEM = decrypted + } + + // Decrypt SSL private key + if len(cd.EncryptedSSLPrivateKey) > 0 { + decrypted, err := cipher.Decrypt(cd.EncryptedSSLPrivateKey, encryptionKey) + if err != nil { + return fmt.Errorf("cannot decrypt SSL private key: %w", err) + } + cd.SSLPrivateKeyPEM = decrypted + } return nil } -func (cds *CustomDomains) ListDomainsForRenewal( +func (domains *CustomDomains) ListDomainsForRenewal( ctx context.Context, conn pg.Conn, scope Scoper, @@ -557,10 +688,10 @@ SELECT id, organization_id, domain, - verification_status, - verification_method, - encrypted_verification_token, - acme_challenge_record, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, encrypted_ssl_certificate, encrypted_ssl_private_key, ssl_certificate_chain, @@ -568,52 +699,51 @@ SELECT ssl_expires_at, is_active, created_at, - updated_at, - verified_at, - last_verification_attempt, - verification_attempts + updated_at FROM custom_domains WHERE %s - is_active = true - AND ssl_status = @ssl_status - AND ssl_expires_at < NOW() + INTERVAL '30 days' + AND ssl_status = 'ACTIVE' + AND ssl_expires_at IS NOT NULL + AND ssl_expires_at <= CURRENT_TIMESTAMP + INTERVAL '30 days' ORDER BY ssl_expires_at ASC ` q = fmt.Sprintf(q, scope.SQLFragment()) - args := pgx.NamedArgs{"ssl_status": CustomDomainSSLStatusActive} + args := pgx.NamedArgs{} maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { - return fmt.Errorf("cannot query domains for renewal: %w", err) + return fmt.Errorf("cannot query custom domains for renewal: %w", err) } - customDomains, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CustomDomain]) + result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CustomDomain]) if err != nil { - return fmt.Errorf("cannot collect domains: %w", err) + return fmt.Errorf("cannot collect custom domains: %w", err) } - *cds = customDomains - + *domains = result return nil } -func (cds *CustomDomains) LoadActiveCertificates(ctx context.Context, conn pg.Conn, scope Scoper) error { +func (domains *CustomDomains) ListDomainsWithPendingHTTPChallenges( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { q := ` SELECT id, - tenant_id, organization_id, domain, - verification_status, - verification_method, - encrypted_verification_token, - acme_challenge_record, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, encrypted_ssl_certificate, encrypted_ssl_private_key, ssl_certificate_chain, @@ -621,27 +751,68 @@ SELECT ssl_expires_at, is_active, created_at, - updated_at, - verified_at, - last_verification_attempt, - verification_attempts + updated_at FROM custom_domains WHERE %s - is_active = @is_active - AND encrypted_ssl_certificate IS NOT NULL - AND ssl_status = @ssl_status -ORDER BY - ssl_expires_at DESC - ` + AND http_challenge_token IS NOT NULL + AND ssl_status IN ('PROVISIONING', 'RENEWING') +` q = fmt.Sprintf(q, scope.SQLFragment()) - args := pgx.NamedArgs{ - "is_active": true, - "ssl_status": CustomDomainSSLStatusActive, + args := pgx.NamedArgs{} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query custom domains with pending challenges: %w", err) } + + result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CustomDomain]) + if err != nil { + return fmt.Errorf("cannot collect custom domains: %w", err) + } + + *domains = result + return nil +} + +func (domains *CustomDomains) LoadActiveCertificates( + ctx context.Context, + conn pg.Conn, + scope Scoper, + encryptionKey cipher.EncryptionKey, +) error { + q := ` +SELECT + id, + organization_id, + domain, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, + encrypted_ssl_certificate, + encrypted_ssl_private_key, + ssl_certificate_chain, + ssl_status, + ssl_expires_at, + is_active, + created_at, + updated_at +FROM + custom_domains +WHERE + %s + AND ssl_status = 'ACTIVE' + AND encrypted_ssl_certificate IS NOT NULL +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.NamedArgs{} maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) @@ -649,12 +820,31 @@ ORDER BY return fmt.Errorf("cannot query active certificates: %w", err) } - customDomains, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CustomDomain]) + result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[CustomDomain]) if err != nil { - return fmt.Errorf("cannot collect active certificates: %w", err) + return fmt.Errorf("cannot collect custom domains: %w", err) } - *cds = customDomains + for _, cd := range result { + // Decrypt SSL certificate + if len(cd.EncryptedSSLCertificate) > 0 { + decrypted, err := cipher.Decrypt(cd.EncryptedSSLCertificate, encryptionKey) + if err != nil { + return fmt.Errorf("cannot decrypt SSL certificate: %w", err) + } + cd.SSLCertificatePEM = decrypted + } + // Decrypt SSL private key + if len(cd.EncryptedSSLPrivateKey) > 0 { + decrypted, err := cipher.Decrypt(cd.EncryptedSSLPrivateKey, encryptionKey) + if err != nil { + return fmt.Errorf("cannot decrypt SSL private key: %w", err) + } + cd.SSLPrivateKeyPEM = decrypted + } + } + + *domains = result return nil } diff --git a/pkg/coredata/custom_domain_order_field.go b/pkg/coredata/custom_domain_order_field.go index 998c9cd9c..6f3d6482a 100644 --- a/pkg/coredata/custom_domain_order_field.go +++ b/pkg/coredata/custom_domain_order_field.go @@ -21,6 +21,7 @@ type CustomDomainOrderField string const ( CustomDomainOrderFieldCreatedAt CustomDomainOrderField = "CREATED_AT" CustomDomainOrderFieldDomain CustomDomainOrderField = "DOMAIN" + CustomDomainOrderFieldUpdatedAt CustomDomainOrderField = "UPDATED_AT" ) func (f CustomDomainOrderField) Column() string { @@ -29,6 +30,8 @@ func (f CustomDomainOrderField) Column() string { return "created_at" case CustomDomainOrderFieldDomain: return "domain" + case CustomDomainOrderFieldUpdatedAt: + return "updated_at" default: panic(fmt.Sprintf("unsupported order by: %s", f)) }