Scert/backend/internal/repository/sql_domain_repository.go

236 lines
7.8 KiB
Go

package repository
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
)
type SQLDomainRepository struct {
db *sql.DB
driver string
}
func NewSQLDomainRepository(db *sql.DB, driver ...string) *SQLDomainRepository {
return &SQLDomainRepository{db: db, driver: repositoryDriver(driver)}
}
func (r *SQLDomainRepository) query(value string) string {
return Rebind(r.driver, value)
}
func (r *SQLDomainRepository) Create(ctx context.Context, item domain.Domain) error {
if r == nil || r.db == nil {
return errors.New("数据库连接不能为空")
}
payload, err := json.Marshal(item.SANs)
if err != nil {
return fmt.Errorf("编码 SAN 域名失败:%w", err)
}
challengeType, err := domain.NormalizeChallengeType(item.ChallengeType, append([]string{item.Name}, item.SANs...))
if err != nil {
return err
}
_, err = r.db.ExecContext(ctx, r.query(`INSERT INTO domains
(id, name, certificate_type, sans_json, auto_renew, acme_environment, dns_provider, status, expires_at, created_at, updated_at, project_id, owner_id, created_by, updated_by, challenge_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), item.ID, item.Name, item.CertificateType, string(payload), item.AutoRenew,
item.ACMEEnvironment, item.DNSProvider, item.Status, nullableTime(item.ExpiresAt), item.CreatedAt, item.UpdatedAt,
item.ResourceScope.Normalize().ProjectID, item.OwnerID, item.CreatedBy, item.UpdatedBy, challengeType)
if err != nil {
return fmt.Errorf("保存域名失败:%w", err)
}
return nil
}
func (r *SQLDomainRepository) Update(ctx context.Context, item domain.Domain) error {
if r == nil || r.db == nil {
return errors.New("数据库连接不能为空")
}
payload, err := json.Marshal(item.SANs)
if err != nil {
return fmt.Errorf("编码 SAN 域名失败:%w", err)
}
challengeType, err := domain.NormalizeChallengeType(item.ChallengeType, append([]string{item.Name}, item.SANs...))
if err != nil {
return err
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
var storedID string
if err := tx.QueryRowContext(ctx, r.query(`SELECT id FROM domains WHERE id = ? AND project_id = ?`+taskResourceWriteLock(r.driver)), item.ID, item.ResourceScope.Normalize().ProjectID).Scan(&storedID); err != nil {
return err
}
if err := validateBoundDomainDNS(ctx, tx, r.driver, item, challengeType); err != nil {
return err
}
if err := NewSQLProjectMemberRepository(r.db, r.driver).rejectActiveDNSLease(ctx, tx, "domain_id", item.ID); err != nil {
return err
}
_, err = tx.ExecContext(ctx, r.query(`UPDATE domains SET
name = ?, certificate_type = ?, sans_json = ?, auto_renew = ?, acme_environment = ?, dns_provider = ?, updated_at = ?, updated_by = ?, challenge_type = ?
WHERE id = ? AND project_id = ?`), item.Name, item.CertificateType, string(payload), item.AutoRenew, item.ACMEEnvironment,
item.DNSProvider, item.UpdatedAt, item.UpdatedBy, challengeType, item.ID, item.ResourceScope.Normalize().ProjectID)
if err != nil {
return fmt.Errorf("更新域名失败:%w", err)
}
return tx.Commit()
}
func (r *SQLDomainRepository) GetByID(ctx context.Context, id string) (domain.Domain, error) {
if r == nil || r.db == nil {
return domain.Domain{}, errors.New("数据库连接不能为空")
}
row := r.db.QueryRowContext(ctx, r.query(visibleDomainSelect+` WHERE domains.id = ?`), id)
return scanDomain(row)
}
func (r *SQLDomainRepository) List(ctx context.Context) ([]domain.Domain, error) {
if r == nil || r.db == nil {
return nil, errors.New("数据库连接不能为空")
}
rows, err := r.db.QueryContext(ctx, r.query(visibleDomainSelect+` ORDER BY domains.name`))
if err != nil {
return nil, fmt.Errorf("查询域名失败:%w", err)
}
defer rows.Close()
items := make([]domain.Domain, 0)
for rows.Next() {
item, err := scanDomain(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取域名结果失败:%w", err)
}
return items, nil
}
func (r *SQLDomainRepository) Delete(ctx context.Context, id string) error {
if r == nil || r.db == nil {
return errors.New("数据库连接不能为空")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("开始删除域名失败:%w", err)
}
rollback := func(cause error) error {
_ = tx.Rollback()
return cause
}
var storedID string
if err := tx.QueryRowContext(ctx, r.query(`SELECT id FROM domains WHERE id = ?`+taskResourceWriteLock(r.driver)), id).Scan(&storedID); err != nil {
return rollback(err)
}
if err := NewSQLProjectMemberRepository(r.db, r.driver).rejectActiveDNSLease(ctx, tx, "domain_id", id); err != nil {
return rollback(err)
}
if _, err := tx.ExecContext(ctx, r.query("DELETE FROM domain_servers WHERE domain_id = ?"), id); err != nil {
return rollback(fmt.Errorf("删除域名服务器关联失败:%w", err))
}
if _, err := tx.ExecContext(ctx, r.query("DELETE FROM domain_dns_accounts WHERE domain_id = ?"), id); err != nil {
return rollback(err)
}
result, err := tx.ExecContext(ctx, r.query("DELETE FROM domains WHERE id = ?"), id)
if err != nil {
return rollback(fmt.Errorf("删除域名失败:%w", err))
}
count, err := result.RowsAffected()
if err != nil {
return rollback(fmt.Errorf("读取删除结果失败:%w", err))
}
if count == 0 {
return rollback(sql.ErrNoRows)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("提交域名删除失败:%w", err)
}
return nil
}
func (r *SQLDomainRepository) UpdateCertificateState(ctx context.Context, id, status string, expiresAt *time.Time) error {
if r == nil || r.db == nil {
return errors.New("数据库连接不能为空")
}
status = strings.TrimSpace(status)
if status == "" {
return errors.New("证书状态不能为空")
}
result, err := r.db.ExecContext(ctx, r.query(`UPDATE domains SET status = ?, expires_at = ?, updated_at = ? WHERE id = ?`),
status, nullableTime(expiresAt), time.Now().UTC(), id)
if err != nil {
return fmt.Errorf("更新证书状态失败:%w", err)
}
count, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("读取证书状态更新结果失败:%w", err)
}
if count == 0 {
return sql.ErrNoRows
}
return nil
}
type scanner interface {
Scan(dest ...any) error
}
func scanDomain(row scanner) (domain.Domain, error) {
var item domain.Domain
var certificateType string
var sansJSON string
var autoRenew bool
var acmeEnvironment string
var dnsProvider string
var expiresAt sql.NullTime
if err := row.Scan(&item.ID, &item.Name, &certificateType, &sansJSON, &autoRenew, &acmeEnvironment, &dnsProvider, &item.Status, &expiresAt, &item.CreatedAt, &item.UpdatedAt, &item.ProjectID, &item.OwnerID, &item.CreatedBy, &item.UpdatedBy, &item.ChallengeType, &item.DNSAccountID, &item.DNSBindingID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.Domain{}, sql.ErrNoRows
}
return domain.Domain{}, fmt.Errorf("读取域名失败:%w", err)
}
if err := json.Unmarshal([]byte(sansJSON), &item.SANs); err != nil {
return domain.Domain{}, fmt.Errorf("解析 SAN 域名失败:%w", err)
}
item.CertificateType = domain.CertificateType(certificateType)
item.AutoRenew = autoRenew
item.ACMEEnvironment = acmeEnvironment
item.DNSProvider = dnsProvider
if expiresAt.Valid {
value := expiresAt.Time
item.ExpiresAt = &value
}
return item, nil
}
func nullableTime(value *time.Time) any {
if value == nil {
return nil
}
return value.UTC()
}
func IsNotFound(err error) bool {
return err != nil && errors.Is(err, sql.ErrNoRows)
}
func IsConflict(err error) bool {
if err == nil {
return false
}
message := strings.ToLower(err.Error())
return strings.Contains(message, "duplicate") ||
strings.Contains(message, "unique constraint") ||
strings.Contains(message, "constraint failed")
}