Monorepo for Tangled

appview/settings: add pages domain claiming

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

anirudh.fi 2403d3f9 dc25ab48

verified
+127
+127
appview/settings/settings.go
··· 64 64 r.Put("/", s.updateNotificationPreferences) 65 65 }) 66 66 67 + r.Route("/pages", func(r chi.Router) { 68 + r.Get("/", s.pagesSettings) 69 + r.Put("/", s.claimPagesDomain) 70 + r.Delete("/", s.releasePagesDomain) 71 + }) 72 + 67 73 return r 74 + } 75 + 76 + func (s *Settings) pagesSettings(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 + log.Printf("failed to get domain claim: %s", err) 83 + claim = nil 84 + } 85 + 86 + // Determine whether the active account has a tngl.sh handle, in which 87 + // case their pages 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.UserPagesSettings(w, pages.UserPagesSettingsParams{ 99 + LoggedInUser: user, 100 + Claim: claim, 101 + PagesDomain: s.Config.Pages.Domain, 102 + IsTnglHandle: isTnglHandle, 103 + }) 104 + } 105 + 106 + func (s *Settings) claimPagesDomain(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-pages-error", "Subdomain cannot be empty.") 112 + return 113 + } 114 + 115 + // Validate subdomain characters: lowercase letters, digits, hyphens; no leading/trailing hyphen 116 + if !isValidSubdomain(subdomain) { 117 + s.Pages.Notice(w, "settings-pages-error", "Invalid subdomain. Use only lowercase letters, digits, and hyphens. Cannot start or end with a hyphen.") 118 + return 119 + } 120 + 121 + pagesDomain := s.Config.Pages.Domain 122 + 123 + // Disallow claiming the root pages domain itself 124 + if subdomain == pagesDomain { 125 + s.Pages.Notice(w, "settings-pages-error", fmt.Sprintf("You cannot claim the root domain %q.", pagesDomain)) 126 + return 127 + } 128 + 129 + fullDomain := subdomain + "." + pagesDomain 130 + 131 + if err := db.ClaimDomain(s.Db, did, fullDomain); err != nil { 132 + switch { 133 + case errors.Is(err, db.ErrDomainTaken): 134 + s.Pages.Notice(w, "settings-pages-error", "That domain is already claimed by another user.") 135 + case errors.Is(err, db.ErrDomainCooldown): 136 + s.Pages.Notice(w, "settings-pages-error", "That domain was recently released and is in a 30-day cooldown period. Please try again later.") 137 + case errors.Is(err, db.ErrAlreadyClaimed): 138 + s.Pages.Notice(w, "settings-pages-error", "You already have a domain claimed. Release it before claiming a new one.") 139 + default: 140 + log.Printf("claiming domain: %s", err) 141 + s.Pages.Notice(w, "settings-pages-error", "Unable to claim domain at this moment. Try again later.") 142 + } 143 + return 144 + } 145 + 146 + s.Pages.Notice(w, "settings-pages-success", fmt.Sprintf("Domain %q successfully claimed.", fullDomain)) 147 + } 148 + 149 + func (s *Settings) releasePagesDomain(w http.ResponseWriter, r *http.Request) { 150 + did := s.OAuth.GetDid(r) 151 + domain := strings.TrimSpace(r.FormValue("domain")) 152 + 153 + if domain == "" { 154 + s.Pages.Notice(w, "settings-pages-error", "Domain cannot be empty.") 155 + return 156 + } 157 + 158 + // Disallow releasing an auto-assigned PDS handle domain. 159 + pdsDomain := strings.TrimPrefix(s.Config.Pds.Host, "https://") 160 + pdsDomain = strings.TrimPrefix(pdsDomain, "http://") 161 + user := s.OAuth.GetMultiAccountUser(r) 162 + for _, acc := range user.Accounts { 163 + if acc.Did == did && strings.HasSuffix(acc.Handle, "."+pdsDomain) { 164 + if strings.HasSuffix(domain, "."+pdsDomain) { 165 + s.Pages.Notice(w, "settings-pages-error", "Your tngl.sh domain is tied to your handle and cannot be released here.") 166 + return 167 + } 168 + } 169 + } 170 + 171 + if err := db.ReleaseDomain(s.Db, did, domain); err != nil { 172 + log.Printf("releasing domain: %s", err) 173 + s.Pages.Notice(w, "settings-pages-error", "Unable to release domain. Make sure it belongs to your account.") 174 + return 175 + } 176 + 177 + s.Pages.HxLocation(w, "/settings/pages") 178 + } 179 + 180 + // isValidSubdomain checks that a subdomain label uses only lowercase letters, 181 + // digits, and hyphens, and does not start or end with a hyphen. 182 + func isValidSubdomain(s string) bool { 183 + if len(s) == 0 || len(s) > 63 { 184 + return false 185 + } 186 + if s[0] == '-' || s[len(s)-1] == '-' { 187 + return false 188 + } 189 + for _, c := range s { 190 + if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') { 191 + return false 192 + } 193 + } 194 + return true 68 195 } 69 196 70 197 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) {