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)
18
19type Signup struct {
20 config *config.Config
21 db *db.DB
22 cf *dns.Cloudflare
23 posthog posthog.Client
24 xrpc *xrpcclient.Client
25 idResolver *idresolver.Resolver
26 pages *pages.Pages
27 l *slog.Logger
28}
29
30func New(cfg *config.Config, cf *dns.Cloudflare, database *db.DB, pc posthog.Client, idResolver *idresolver.Resolver, pages *pages.Pages, l *slog.Logger) *Signup {
31 return &Signup{
32 config: cfg,
33 db: database,
34 cf: cf,
35 posthog: pc,
36 idResolver: idResolver,
37 pages: pages,
38 l: l,
39 }
40}
41
42func (s *Signup) Router() http.Handler {
43 r := chi.NewRouter()
44 r.Post("/", s.signup)
45 r.Get("/complete", s.complete)
46 r.Post("/complete", s.complete)
47
48 return r
49}
50
51func (s *Signup) signup(w http.ResponseWriter, r *http.Request) {
52 emailId := r.FormValue("email")
53
54 if !email.IsValidEmail(emailId) {
55 s.pages.Notice(w, "login-msg", "Invalid email address.")
56 return
57 }
58
59 exists, err := db.CheckEmailExistsAtAll(s.db, emailId)
60 if err != nil {
61 s.l.Error("failed to check email existence", "error", err)
62 s.pages.Notice(w, "login-msg", "Failed to complete signup. Try again later.")
63 return
64 }
65 if exists {
66 s.pages.Notice(w, "login-msg", "Email already exists.")
67 return
68 }
69
70 code, err := s.inviteCodeRequest()
71 if err != nil {
72 s.l.Error("failed to create invite code", "error", err)
73 s.pages.Notice(w, "login-msg", "Failed to create invite code.")
74 return
75 }
76
77 em := email.Email{
78 APIKey: s.config.Resend.ApiKey,
79 From: s.config.Resend.SentFrom,
80 To: emailId,
81 Subject: "Verify your Tangled account",
82 Text: `Copy and paste this code below to verify your account on Tangled.
83 ` + code,
84 Html: `<p>Copy and paste this code below to verify your account on Tangled.</p>
85<p><code>` + code + `</code></p>`,
86 }
87
88 err = email.SendEmail(em)
89 if err != nil {
90 s.l.Error("failed to send email", "error", err)
91 s.pages.Notice(w, "login-msg", "Failed to send email.")
92 return
93 }
94 err = db.AddInflightSignup(s.db, db.InflightSignup{
95 Email: emailId,
96 InviteCode: code,
97 })
98 if err != nil {
99 s.l.Error("failed to add inflight signup", "error", err)
100 s.pages.Notice(w, "login-msg", "Failed to complete sign up. Try again later.")
101 return
102 }
103
104 s.pages.HxRedirect(w, "/signup/complete")
105}
106
107func (s *Signup) complete(w http.ResponseWriter, r *http.Request) {
108 switch r.Method {
109 case http.MethodGet:
110 s.pages.CompleteSignup(w, pages.SignupParams{})
111 case http.MethodPost:
112 username := r.FormValue("username")
113 password := r.FormValue("password")
114 code := r.FormValue("code")
115
116 if !userutil.IsValidSubdomain(username) {
117 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.")
118 return
119 }
120
121 email, err := db.GetEmailForCode(s.db, code)
122 if err != nil {
123 s.l.Error("failed to get email for code", "error", err)
124 s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.")
125 return
126 }
127
128 did, err := s.createAccountRequest(username, password, email, code)
129 if err != nil {
130 s.l.Error("failed to create account", "error", err)
131 s.pages.Notice(w, "signup-error", err.Error())
132 return
133 }
134
135 err = s.cf.CreateDNSRecord(r.Context(), dns.Record{
136 Type: "TXT",
137 Name: "_atproto." + username,
138 Content: "did=" + did,
139 TTL: 6400,
140 Proxied: false,
141 })
142 if err != nil {
143 s.l.Error("failed to create DNS record", "error", err)
144 s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.")
145 return
146 }
147
148 err = db.AddEmail(s.db, db.Email{
149 Did: did,
150 Address: email,
151 Verified: true,
152 Primary: true,
153 })
154 if err != nil {
155 s.l.Error("failed to add email", "error", err)
156 s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.")
157 return
158 }
159
160 s.pages.Notice(w, "signup-msg", fmt.Sprintf(`Account created successfully. You can now
161 <a class="underline text-black dark:text-white" href="/login">login</a>
162 with <code>%s.tngl.sh</code>.`, username))
163
164 go func() {
165 err := db.DeleteInflightSignup(s.db, email)
166 if err != nil {
167 s.l.Error("failed to delete inflight signup", "error", err)
168 }
169 }()
170 return
171 }
172}