internal/store/pagedomains.go
124 lines · 3678 bytes
11 symbols in this file
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}