forked from
tangled.org/core
Monorepo for Tangled
1package db
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "time"
8
9 "tangled.org/core/appview/models"
10)
11
12var (
13 ErrDomainTaken = errors.New("domain is already claimed by another user")
14 ErrDomainCooldown = errors.New("domain is in a 30-day cooldown period after being released")
15 ErrAlreadyClaimed = errors.New("you already have an active domain claim; release it before claiming a new one")
16)
17
18func scanClaim(row *sql.Row) (*models.DomainClaim, error) {
19 var claim models.DomainClaim
20 var deletedStr sql.NullString
21
22 if err := row.Scan(&claim.ID, &claim.Did, &claim.Domain, &deletedStr); err != nil {
23 return nil, err
24 }
25
26 if deletedStr.Valid {
27 t, err := time.Parse(time.RFC3339, deletedStr.String)
28 if err != nil {
29 return nil, fmt.Errorf("parsing deleted timestamp: %w", err)
30 }
31 claim.Deleted = &t
32 }
33
34 return &claim, nil
35}
36
37func GetDomainClaimByDomain(e Execer, domain string) (*models.DomainClaim, error) {
38 row := e.QueryRow(`
39 select id, did, domain, deleted
40 from domain_claims
41 where domain = ?
42 `, domain)
43 return scanClaim(row)
44}
45
46func GetActiveDomainClaimForDid(e Execer, did string) (*models.DomainClaim, error) {
47 row := e.QueryRow(`
48 select id, did, domain, deleted
49 from domain_claims
50 where did = ? and deleted is null
51 `, did)
52 return scanClaim(row)
53}
54
55func GetDomainClaimForDid(e Execer, did string) (*models.DomainClaim, error) {
56 row := e.QueryRow(`
57 select id, did, domain, deleted
58 from domain_claims
59 where did = ?
60 `, did)
61 return scanClaim(row)
62}
63
64func ClaimDomain(e Execer, did, domain string) error {
65 const cooldown = 30 * 24 * time.Hour
66
67 domainRow, err := GetDomainClaimByDomain(e, domain)
68 if err != nil && !errors.Is(err, sql.ErrNoRows) {
69 return fmt.Errorf("looking up domain: %w", err)
70 }
71
72 if domainRow != nil {
73 if domainRow.Did == did {
74 if domainRow.Deleted == nil {
75 return nil
76 }
77 if time.Since(*domainRow.Deleted) < cooldown {
78 return ErrDomainCooldown
79 }
80 _, err = e.Exec(`
81 update domain_claims set deleted = null where did = ? and domain = ?
82 `, did, domain)
83 return err
84 }
85
86 if domainRow.Deleted == nil {
87 return ErrDomainTaken
88 }
89 if time.Since(*domainRow.Deleted) < cooldown {
90 return ErrDomainCooldown
91 }
92
93 if _, err = e.Exec(`delete from domain_claims where domain = ?`, domain); err != nil {
94 return fmt.Errorf("clearing expired domain row: %w", err)
95 }
96 }
97
98 didRow, err := GetDomainClaimForDid(e, did)
99 if err != nil && !errors.Is(err, sql.ErrNoRows) {
100 return fmt.Errorf("looking up DID claim: %w", err)
101 }
102
103 if didRow == nil {
104 _, err = e.Exec(`
105 insert into domain_claims (did, domain) values (?, ?)
106 `, did, domain)
107 return err
108 }
109
110 if didRow.Deleted == nil {
111 return ErrAlreadyClaimed
112 }
113
114 _, err = e.Exec(`
115 update domain_claims set domain = ?, deleted = null where did = ?
116 `, domain, did)
117 return err
118}
119
120func ReleaseDomain(e Execer, did, domain string) error {
121 result, err := e.Exec(`
122 update domain_claims
123 set deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
124 where did = ? and domain = ? and deleted is null
125 `, did, domain)
126 if err != nil {
127 return err
128 }
129
130 n, err := result.RowsAffected()
131 if err != nil {
132 return err
133 }
134 if n == 0 {
135 return errors.New("domain not found or not actively claimed by this account")
136 }
137 return nil
138}
139
140// GetRepoSiteConfig returns the site configuration for a repo, or nil if not configured.
141func GetRepoSiteConfig(e Execer, repoAt string) (*models.RepoSite, error) {
142 row := e.QueryRow(`
143 select id, repo_at, branch, dir, is_index, created, updated
144 from repo_sites
145 where repo_at = ?
146 `, repoAt)
147
148 var s models.RepoSite
149 var isIndex int
150 var createdStr, updatedStr string
151
152 err := row.Scan(&s.ID, &s.RepoAt, &s.Branch, &s.Dir, &isIndex, &createdStr, &updatedStr)
153 if errors.Is(err, sql.ErrNoRows) {
154 return nil, nil
155 }
156 if err != nil {
157 return nil, err
158 }
159
160 s.IsIndex = isIndex != 0
161
162 s.Created, err = time.Parse(time.RFC3339, createdStr)
163 if err != nil {
164 return nil, fmt.Errorf("parsing created timestamp: %w", err)
165 }
166
167 s.Updated, err = time.Parse(time.RFC3339, updatedStr)
168 if err != nil {
169 return nil, fmt.Errorf("parsing updated timestamp: %w", err)
170 }
171
172 return &s, nil
173}
174
175// SetRepoSiteConfig inserts or replaces the site configuration for a repo.
176func SetRepoSiteConfig(e Execer, repoAt, branch, dir string, isIndex bool) error {
177 isIndexInt := 0
178 if isIndex {
179 isIndexInt = 1
180 }
181
182 _, err := e.Exec(`
183 insert into repo_sites (repo_at, branch, dir, is_index, updated)
184 values (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
185 on conflict(repo_at) do update set
186 branch = excluded.branch,
187 dir = excluded.dir,
188 is_index = excluded.is_index,
189 updated = excluded.updated
190 `, repoAt, branch, dir, isIndexInt)
191 return err
192}
193
194// DeleteRepoSiteConfig removes the site configuration for a repo.
195func DeleteRepoSiteConfig(e Execer, repoAt string) error {
196 _, err := e.Exec(`delete from repo_sites where repo_at = ?`, repoAt)
197 return err
198}
199
200// GetRepoSiteConfigsForDid returns all site configurations for repos owned by a DID.
201// RepoName is populated on each returned RepoSite.
202func GetRepoSiteConfigsForDid(e Execer, did string) ([]*models.RepoSite, error) {
203 rows, err := e.Query(`
204 select rs.id, rs.repo_at, r.name, rs.branch, rs.dir, rs.is_index, rs.created, rs.updated
205 from repo_sites rs
206 join repos r on r.at_uri = rs.repo_at
207 where r.did = ?
208 `, did)
209 if err != nil {
210 return nil, err
211 }
212 defer rows.Close()
213
214 var sites []*models.RepoSite
215 for rows.Next() {
216 var s models.RepoSite
217 var isIndex int
218 var createdStr, updatedStr string
219 if err := rows.Scan(&s.ID, &s.RepoAt, &s.RepoName, &s.Branch, &s.Dir, &isIndex, &createdStr, &updatedStr); err != nil {
220 return nil, err
221 }
222 s.IsIndex = isIndex != 0
223 s.Created, err = time.Parse(time.RFC3339, createdStr)
224 if err != nil {
225 return nil, fmt.Errorf("parsing created timestamp: %w", err)
226 }
227 s.Updated, err = time.Parse(time.RFC3339, updatedStr)
228 if err != nil {
229 return nil, fmt.Errorf("parsing updated timestamp: %w", err)
230 }
231 sites = append(sites, &s)
232 }
233 return sites, rows.Err()
234}
235
236// DeleteRepoSiteConfigsForDid removes all site configurations for repos owned by a DID.
237func DeleteRepoSiteConfigsForDid(e Execer, did string) error {
238 _, err := e.Exec(`
239 delete from repo_sites
240 where repo_at in (
241 select at_uri from repos where did = ?
242 )
243 `, did)
244 return err
245}
246
247// GetIndexRepoAtForDid returns the repo_at of the repo that currently holds
248// is_index=1 for the given DID, excluding excludeRepoAt (the current repo).
249// Returns "", nil if no other repo is the index site.
250func GetIndexRepoAtForDid(e Execer, did, excludeRepoAt string) (string, error) {
251 row := e.QueryRow(`
252 select rs.repo_at
253 from repo_sites rs
254 join repos r on r.at_uri = rs.repo_at
255 where r.did = ?
256 and rs.is_index = 1
257 and rs.repo_at != ?
258 limit 1
259 `, did, excludeRepoAt)
260
261 var repoAt string
262 err := row.Scan(&repoAt)
263 if errors.Is(err, sql.ErrNoRows) {
264 return "", nil
265 }
266 if err != nil {
267 return "", err
268 }
269 return repoAt, nil
270}