internal/store/pagedomains.go

v1.2.0
gitbay/internal/store/pagedomains.go history · blame · raw

124 lines · 3678 bytes

  1package store
  2
  3import (
  4	"database/sql"
  5	"errors"
  6)
  7
  8// PageDomain is one custom-domain claim. A claim starts pending — it holds
  9// the domain but serves nothing — and activates when the DNS challenge
 10// verifies. Pending claims expire so a squatted claim frees itself.
 11type PageDomain struct {
 12	Domain     string
 13	RepoID     int64
 14	UserID     int64
 15	Token      string
 16	CreatedAt  string
 17	VerifiedAt string
 18}
 19
 20func (d PageDomain) Verified() bool { return d.VerifiedAt != "" }
 21
 22// AddPageDomain claims a domain for a repo. Expired pending claims (any
 23// repo's) are cleared first, so abandonment frees the name; live claims
 24// make the insert fail with ErrExists.
 25func (s *Store) AddPageDomain(domain string, repoID, userID int64, token string, ttlSeconds int) error {
 26	if _, err := s.DB.Exec(
 27		"DELETE FROM page_domains WHERE domain = ? AND verified_at = '' AND strftime('%s','now') - strftime('%s', created_at) > ?",
 28		domain, ttlSeconds); err != nil {
 29		return err
 30	}
 31	_, err := s.DB.Exec(
 32		"INSERT INTO page_domains (domain, repo_id, user_id, token) VALUES (?, ?, ?, ?)",
 33		domain, repoID, userID, token)
 34	if err != nil && isUniqueErr(err) {
 35		return ErrExists
 36	}
 37	return err
 38}
 39
 40func (s *Store) RemovePageDomain(domain string, repoID int64) error {
 41	res, err := s.DB.Exec("DELETE FROM page_domains WHERE domain = ? AND repo_id = ?", domain, repoID)
 42	if err != nil {
 43		return err
 44	}
 45	if n, _ := res.RowsAffected(); n == 0 {
 46		return ErrNotFound
 47	}
 48	return nil
 49}
 50
 51const pageDomainSelect = "SELECT domain, repo_id, user_id, token, created_at, verified_at FROM page_domains"
 52
 53func scanPageDomain(row interface{ Scan(...any) error }) (PageDomain, error) {
 54	var d PageDomain
 55	err := row.Scan(&d.Domain, &d.RepoID, &d.UserID, &d.Token, &d.CreatedAt, &d.VerifiedAt)
 56	return d, err
 57}
 58
 59// PageDomainClaim returns a repo's claim on a domain, verified or pending.
 60func (s *Store) PageDomainClaim(domain string, repoID int64) (PageDomain, error) {
 61	d, err := scanPageDomain(s.DB.QueryRow(
 62		pageDomainSelect+" WHERE domain = ? AND repo_id = ?", domain, repoID))
 63	if errors.Is(err, sql.ErrNoRows) {
 64		return d, ErrNotFound
 65	}
 66	return d, err
 67}
 68
 69// PageDomainExpired reports whether a pending claim has outlived the TTL.
 70func (s *Store) PageDomainExpired(d PageDomain, ttlSeconds int) bool {
 71	if d.Verified() {
 72		return false
 73	}
 74	var expired bool
 75	s.DB.QueryRow(
 76		"SELECT strftime('%s','now') - strftime('%s', ?) > ?", d.CreatedAt, ttlSeconds).Scan(&expired)
 77	return expired
 78}
 79
 80// VerifyPageDomain activates a pending claim.
 81func (s *Store) VerifyPageDomain(domain string, repoID int64) error {
 82	res, err := s.DB.Exec(
 83		"UPDATE page_domains SET verified_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE domain = ? AND repo_id = ?",
 84		domain, repoID)
 85	if err != nil {
 86		return err
 87	}
 88	if n, _ := res.RowsAffected(); n == 0 {
 89		return ErrNotFound
 90	}
 91	return nil
 92}
 93
 94func (s *Store) ListPageDomains(repoID int64) ([]PageDomain, error) {
 95	rows, err := s.DB.Query(pageDomainSelect+" WHERE repo_id = ? ORDER BY domain", repoID)
 96	if err != nil {
 97		return nil, err
 98	}
 99	defer rows.Close()
100	var out []PageDomain
101	for rows.Next() {
102		d, err := scanPageDomain(rows)
103		if err != nil {
104			return nil, err
105		}
106		out = append(out, d)
107	}
108	return out, rows.Err()
109}
110
111// PageDomainRepo resolves a request host to the repo serving it. Only
112// verified claims serve.
113func (s *Store) PageDomainRepo(domain string) (Repo, error) {
114	var repoID int64
115	err := s.DB.QueryRow(
116		"SELECT repo_id FROM page_domains WHERE domain = ? AND verified_at != ''", domain).Scan(&repoID)
117	if errors.Is(err, sql.ErrNoRows) {
118		return Repo{}, ErrNotFound
119	}
120	if err != nil {
121		return Repo{}, err
122	}
123	return s.RepoByID(repoID)
124}