this repo has no description
1package signup 2 3import ( 4 "fmt" 5 "log/slog" 6 "net/http" 7 8 "github.com/go-chi/chi/v5" 9 "github.com/posthog/posthog-go" 10 "tangled.sh/tangled.sh/core/appview/config" 11 "tangled.sh/tangled.sh/core/appview/db" 12 "tangled.sh/tangled.sh/core/appview/dns" 13 "tangled.sh/tangled.sh/core/appview/email" 14 "tangled.sh/tangled.sh/core/appview/pages" 15 "tangled.sh/tangled.sh/core/appview/state/userutil" 16 "tangled.sh/tangled.sh/core/appview/xrpcclient" 17 "tangled.sh/tangled.sh/core/idresolver" 18) 19 20type Signup struct { 21 config *config.Config 22 db *db.DB 23 cf *dns.Cloudflare 24 posthog posthog.Client 25 xrpc *xrpcclient.Client 26 idResolver *idresolver.Resolver 27 pages *pages.Pages 28 l *slog.Logger 29} 30 31func New(cfg *config.Config, database *db.DB, pc posthog.Client, idResolver *idresolver.Resolver, pages *pages.Pages, l *slog.Logger) *Signup { 32 var cf *dns.Cloudflare 33 if cfg.Cloudflare.ApiToken != "" && cfg.Cloudflare.ZoneId != "" { 34 var err error 35 cf, err = dns.NewCloudflare(cfg) 36 if err != nil { 37 l.Warn("failed to create cloudflare client, signup will be disabled", "error", err) 38 } 39 } 40 41 return &Signup{ 42 config: cfg, 43 db: database, 44 posthog: pc, 45 idResolver: idResolver, 46 cf: cf, 47 pages: pages, 48 l: l, 49 } 50} 51 52func (s *Signup) Router() http.Handler { 53 r := chi.NewRouter() 54 r.Post("/", s.signup) 55 r.Get("/complete", s.complete) 56 r.Post("/complete", s.complete) 57 58 return r 59} 60 61func (s *Signup) signup(w http.ResponseWriter, r *http.Request) { 62 if s.cf == nil { 63 http.Error(w, "signup is disabled", http.StatusFailedDependency) 64 } 65 emailId := r.FormValue("email") 66 67 if !email.IsValidEmail(emailId) { 68 s.pages.Notice(w, "login-msg", "Invalid email address.") 69 return 70 } 71 72 exists, err := db.CheckEmailExistsAtAll(s.db, emailId) 73 if err != nil { 74 s.l.Error("failed to check email existence", "error", err) 75 s.pages.Notice(w, "login-msg", "Failed to complete signup. Try again later.") 76 return 77 } 78 if exists { 79 s.pages.Notice(w, "login-msg", "Email already exists.") 80 return 81 } 82 83 code, err := s.inviteCodeRequest() 84 if err != nil { 85 s.l.Error("failed to create invite code", "error", err) 86 s.pages.Notice(w, "login-msg", "Failed to create invite code.") 87 return 88 } 89 90 em := email.Email{ 91 APIKey: s.config.Resend.ApiKey, 92 From: s.config.Resend.SentFrom, 93 To: emailId, 94 Subject: "Verify your Tangled account", 95 Text: `Copy and paste this code below to verify your account on Tangled. 96 ` + code, 97 Html: `<p>Copy and paste this code below to verify your account on Tangled.</p> 98<p><code>` + code + `</code></p>`, 99 } 100 101 err = email.SendEmail(em) 102 if err != nil { 103 s.l.Error("failed to send email", "error", err) 104 s.pages.Notice(w, "login-msg", "Failed to send email.") 105 return 106 } 107 err = db.AddInflightSignup(s.db, db.InflightSignup{ 108 Email: emailId, 109 InviteCode: code, 110 }) 111 if err != nil { 112 s.l.Error("failed to add inflight signup", "error", err) 113 s.pages.Notice(w, "login-msg", "Failed to complete sign up. Try again later.") 114 return 115 } 116 117 s.pages.HxRedirect(w, "/signup/complete") 118} 119 120func (s *Signup) complete(w http.ResponseWriter, r *http.Request) { 121 switch r.Method { 122 case http.MethodGet: 123 s.pages.CompleteSignup(w, pages.SignupParams{}) 124 case http.MethodPost: 125 username := r.FormValue("username") 126 password := r.FormValue("password") 127 code := r.FormValue("code") 128 129 if !userutil.IsValidSubdomain(username) { 130 s.pages.Notice(w, "signup-error", "Invalid username. Username must be 4–63 characters, lowercase letters, digits, or hyphens, and can't start or end with a hyphen.") 131 return 132 } 133 134 email, err := db.GetEmailForCode(s.db, code) 135 if err != nil { 136 s.l.Error("failed to get email for code", "error", err) 137 s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.") 138 return 139 } 140 141 did, err := s.createAccountRequest(username, password, email, code) 142 if err != nil { 143 s.l.Error("failed to create account", "error", err) 144 s.pages.Notice(w, "signup-error", err.Error()) 145 return 146 } 147 148 if s.cf == nil { 149 s.l.Error("cloudflare client is nil", "error", "Cloudflare integration is not enabled in configuration") 150 s.pages.Notice(w, "signup-error", "Account signup is currently disabled. DNS record creation is not available. Please contact support.") 151 return 152 } 153 154 err = s.cf.CreateDNSRecord(r.Context(), dns.Record{ 155 Type: "TXT", 156 Name: "_atproto." + username, 157 Content: "did=" + did, 158 TTL: 6400, 159 Proxied: false, 160 }) 161 if err != nil { 162 s.l.Error("failed to create DNS record", "error", err) 163 s.pages.Notice(w, "signup-error", "Failed to create DNS record for your handle. Please contact support.") 164 return 165 } 166 167 err = db.AddEmail(s.db, db.Email{ 168 Did: did, 169 Address: email, 170 Verified: true, 171 Primary: true, 172 }) 173 if err != nil { 174 s.l.Error("failed to add email", "error", err) 175 s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.") 176 return 177 } 178 179 s.pages.Notice(w, "signup-msg", fmt.Sprintf(`Account created successfully. You can now 180 <a class="underline text-black dark:text-white" href="/login">login</a> 181 with <code>%s.tngl.sh</code>.`, username)) 182 183 go func() { 184 err := db.DeleteInflightSignup(s.db, email) 185 if err != nil { 186 s.l.Error("failed to delete inflight signup", "error", err) 187 } 188 }() 189 return 190 } 191}