Monorepo for Tangled tangled.org

appview/{db,models}: domain claims models and crud ops

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>

anirudh.fi 598f5cdb 3fd66631

verified
+241
+23
appview/db/db.go
··· 609 609 quote_count integer not null default 0 610 610 ); 611 611 612 + create table if not exists domain_claims ( 613 + id integer primary key autoincrement, 614 + did text not null unique, 615 + domain text not null unique, 616 + deleted text -- timestamp when the domain was released/unclaimed; null means actively claimed 617 + ); 618 + 612 619 create table if not exists migrations ( 613 620 id integer primary key autoincrement, 614 621 name text unique ··· 1251 1258 1252 1259 -- rename new table 1253 1260 alter table profile_stats_new rename to profile_stats; 1261 + `) 1262 + return err 1263 + }) 1264 + 1265 + orm.RunMigration(conn, logger, "add-repo-sites-table", func(tx *sql.Tx) error { 1266 + _, err := tx.Exec(` 1267 + create table if not exists repo_sites ( 1268 + id integer primary key autoincrement, 1269 + repo_at text not null unique, 1270 + branch text not null, 1271 + dir text not null default '/', 1272 + is_index integer not null default 0, 1273 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1274 + updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1275 + foreign key (repo_at) references repos(at_uri) on delete cascade 1276 + ); 1254 1277 `) 1255 1278 return err 1256 1279 })
+198
appview/db/sites.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "errors" 6 + "fmt" 7 + "time" 8 + 9 + "tangled.org/core/appview/models" 10 + ) 11 + 12 + var ( 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 + 18 + func 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 + 37 + func 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 + 46 + func 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 + 55 + func 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 + 64 + func 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 + 120 + func 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. 141 + func 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. 176 + func 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. 195 + func DeleteRepoSiteConfig(e Execer, repoAt string) error { 196 + _, err := e.Exec(`delete from repo_sites where repo_at = ?`, repoAt) 197 + return err 198 + }
+20
appview/models/sites.go
··· 1 + package models 2 + 3 + import "time" 4 + 5 + type DomainClaim struct { 6 + ID int64 7 + Did string 8 + Domain string 9 + Deleted *time.Time 10 + } 11 + 12 + type RepoSite struct { 13 + ID int64 14 + RepoAt string 15 + Branch string 16 + Dir string 17 + IsIndex bool 18 + Created time.Time 19 + Updated time.Time 20 + }