A vibe coded tangled fork which supports pijul.

appview/settings: add pages domain claiming

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

+152
+152
appview/settings/settings.go
··· 64 64 r.Put("/", s.updateNotificationPreferences) 65 65 }) 66 66 67 + r.Route("/sites", func(r chi.Router) { 68 + r.Get("/", s.sitesSettings) 69 + r.Put("/", s.claimSitesDomain) 70 + r.Delete("/", s.releaseSitesDomain) 71 + }) 72 + 67 73 return r 74 + } 75 + 76 + func (s *Settings) sitesSettings(w http.ResponseWriter, r *http.Request) { 77 + user := s.OAuth.GetMultiAccountUser(r) 78 + did := s.OAuth.GetDid(r) 79 + 80 + claim, err := db.GetActiveDomainClaimForDid(s.Db, did) 81 + if err != nil { 82 + s.Logger.Error("failed to get domain claim", "err", err) 83 + claim = nil 84 + } 85 + 86 + // determine whether the active account has a tngl.sh handle, in which 87 + // case their sites domain is automatically their handle domain. 88 + pdsDomain := strings.TrimPrefix(s.Config.Pds.Host, "https://") 89 + pdsDomain = strings.TrimPrefix(pdsDomain, "http://") 90 + isTnglHandle := false 91 + for _, acc := range user.Accounts { 92 + if acc.Did == did && strings.HasSuffix(acc.Handle, "."+pdsDomain) { 93 + isTnglHandle = true 94 + break 95 + } 96 + } 97 + 98 + s.Pages.UserSiteSettings(w, pages.UserSiteSettingsParams{ 99 + LoggedInUser: user, 100 + Claim: claim, 101 + SitesDomain: s.Config.Sites.Domain, 102 + IsTnglHandle: isTnglHandle, 103 + }) 104 + } 105 + 106 + func (s *Settings) claimSitesDomain(w http.ResponseWriter, r *http.Request) { 107 + did := s.OAuth.GetDid(r) 108 + 109 + subdomain := strings.TrimSpace(r.FormValue("subdomain")) 110 + if subdomain == "" { 111 + s.Pages.Notice(w, "settings-sites-error", "Subdomain cannot be empty.") 112 + return 113 + } 114 + 115 + if !isValidSubdomain(subdomain) { 116 + s.Pages.Notice(w, "settings-sites-error", "Invalid subdomain. Use only lowercase letters, digits, and hyphens. Cannot start or end with a hyphen.") 117 + return 118 + } 119 + 120 + sitesDomain := s.Config.Sites.Domain 121 + 122 + if subdomain == sitesDomain { 123 + s.Pages.Notice(w, "settings-sites-error", fmt.Sprintf("You cannot claim the root domain %q.", sitesDomain)) 124 + return 125 + } 126 + 127 + fullDomain := subdomain + "." + sitesDomain 128 + 129 + if err := db.ClaimDomain(s.Db, did, fullDomain); err != nil { 130 + switch { 131 + case errors.Is(err, db.ErrDomainTaken): 132 + s.Pages.Notice(w, "settings-sites-error", "That domain is already claimed by another user.") 133 + case errors.Is(err, db.ErrDomainCooldown): 134 + s.Pages.Notice(w, "settings-sites-error", "That domain was recently released and is in a 30-day cooldown period. Please try again later.") 135 + case errors.Is(err, db.ErrAlreadyClaimed): 136 + s.Pages.Notice(w, "settings-sites-error", "You already have a domain claimed. Release it before claiming a new one.") 137 + default: 138 + s.Logger.Error("claiming domain", "err", err) 139 + s.Pages.Notice(w, "settings-sites-error", "Unable to claim domain at this moment. Try again later.") 140 + } 141 + return 142 + } 143 + 144 + s.Pages.HxRefresh(w) 145 + } 146 + 147 + func (s *Settings) releaseSitesDomain(w http.ResponseWriter, r *http.Request) { 148 + did := s.OAuth.GetDid(r) 149 + domain := strings.TrimSpace(r.FormValue("domain")) 150 + 151 + if domain == "" { 152 + s.Pages.Notice(w, "settings-sites-error", "Domain cannot be empty.") 153 + return 154 + } 155 + 156 + pdsDomain := strings.TrimPrefix(s.Config.Pds.Host, "https://") 157 + pdsDomain = strings.TrimPrefix(pdsDomain, "http://") 158 + user := s.OAuth.GetMultiAccountUser(r) 159 + for _, acc := range user.Accounts { 160 + if acc.Did == did && strings.HasSuffix(acc.Handle, "."+pdsDomain) { 161 + if strings.HasSuffix(domain, "."+pdsDomain) { 162 + s.Pages.Notice(w, "settings-sites-error", "Your tngl.sh domain is tied to your handle and cannot be released here.") 163 + return 164 + } 165 + } 166 + } 167 + 168 + if err := db.ReleaseDomain(s.Db, did, domain); err != nil { 169 + s.Logger.Error("releasing domain", "err", err) 170 + s.Pages.Notice(w, "settings-sites-error", "Unable to release domain. Make sure it belongs to your account.") 171 + return 172 + } 173 + 174 + // Clean up all site data for this DID asynchronously. 175 + if s.CfClient.Enabled() { 176 + siteConfigs, err := db.GetRepoSiteConfigsForDid(s.Db, did) 177 + if err != nil { 178 + s.Logger.Error("releaseSitesDomain: fetching site configs for cleanup", "err", err) 179 + } 180 + 181 + if err := db.DeleteRepoSiteConfigsForDid(s.Db, did); err != nil { 182 + s.Logger.Error("releaseSitesDomain: deleting site configs from db", "err", err) 183 + } 184 + 185 + go func() { 186 + ctx := context.Background() 187 + 188 + // Delete each repo's R2 objects. 189 + for _, sc := range siteConfigs { 190 + if err := sites.Delete(ctx, s.CfClient, did, sc.RepoName); err != nil { 191 + s.Logger.Error("releaseSitesDomain: R2 delete failed", "did", did, "repo", sc.RepoName, "err", err) 192 + } 193 + } 194 + 195 + // Delete the single KV entry for the domain. 196 + if err := sites.DeleteAllDomainMappings(ctx, s.CfClient, domain); err != nil { 197 + s.Logger.Error("releaseSitesDomain: KV delete failed", "domain", domain, "err", err) 198 + } 199 + }() 200 + } 201 + 202 + s.Pages.HxLocation(w, "/settings/sites") 203 + } 204 + 205 + // isValidSubdomain checks that a subdomain label uses only lowercase letters, 206 + // digits, and hyphens, and does not start or end with a hyphen. 207 + func isValidSubdomain(s string) bool { 208 + if len(s) == 0 || len(s) > 63 { 209 + return false 210 + } 211 + if s[0] == '-' || s[len(s)-1] == '-' { 212 + return false 213 + } 214 + for _, c := range s { 215 + if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') { 216 + return false 217 + } 218 + } 219 + return true 68 220 } 69 221 70 222 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) {