this repo has no description
1package settings
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "log"
8 "net/http"
9 "net/url"
10 "strings"
11 "time"
12
13 "github.com/go-chi/chi/v5"
14 "tangled.sh/tangled.sh/core/api/tangled"
15 "tangled.sh/tangled.sh/core/appview"
16 "tangled.sh/tangled.sh/core/appview/auth"
17 "tangled.sh/tangled.sh/core/appview/db"
18 "tangled.sh/tangled.sh/core/appview/email"
19 "tangled.sh/tangled.sh/core/appview/middleware"
20 "tangled.sh/tangled.sh/core/appview/pages"
21
22 comatproto "github.com/bluesky-social/indigo/api/atproto"
23 lexutil "github.com/bluesky-social/indigo/lex/util"
24 "github.com/gliderlabs/ssh"
25 "github.com/google/uuid"
26)
27
28type Settings struct {
29 Db *db.DB
30 Auth *auth.Auth
31 Pages *pages.Pages
32 Config *appview.Config
33}
34
35func (s *Settings) Router(r chi.Router) {
36 r.Use(middleware.AuthMiddleware(s.Auth))
37
38 r.Get("/", s.settings)
39 r.Put("/keys", s.keys)
40 r.Delete("/keys", s.keys)
41 r.Put("/emails", s.emails)
42 r.Delete("/emails", s.emails)
43 r.Get("/emails/verify", s.emailsVerify)
44 r.Post("/emails/verify/resend", s.emailsVerifyResend)
45 r.Post("/emails/primary", s.emailsPrimary)
46
47}
48
49func (s *Settings) settings(w http.ResponseWriter, r *http.Request) {
50 user := s.Auth.GetUser(r)
51 pubKeys, err := db.GetPublicKeys(s.Db, user.Did)
52 if err != nil {
53 log.Println(err)
54 }
55
56 emails, err := db.GetAllEmails(s.Db, user.Did)
57 if err != nil {
58 log.Println(err)
59 }
60
61 s.Pages.Settings(w, pages.SettingsParams{
62 LoggedInUser: user,
63 PubKeys: pubKeys,
64 Emails: emails,
65 })
66}
67
68// buildVerificationEmail creates an email.Email struct for verification emails
69func (s *Settings) buildVerificationEmail(emailAddr, did, code string) email.Email {
70 verifyURL := s.verifyUrl(did, emailAddr, code)
71
72 return email.Email{
73 APIKey: s.Config.ResendApiKey,
74 From: "noreply@notifs.tangled.sh",
75 To: emailAddr,
76 Subject: "Verify your Tangled email",
77 Text: `Click the link below (or copy and paste it into your browser) to verify your email address.
78` + verifyURL,
79 Html: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p>
80<p><a href="` + verifyURL + `">` + verifyURL + `</a></p>`,
81 }
82}
83
84// sendVerificationEmail handles the common logic for sending verification emails
85func (s *Settings) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error {
86 emailToSend := s.buildVerificationEmail(emailAddr, did, code)
87
88 err := email.SendEmail(emailToSend)
89 if err != nil {
90 log.Printf("sending email: %s", err)
91 s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext))
92 return err
93 }
94
95 return nil
96}
97
98func (s *Settings) emails(w http.ResponseWriter, r *http.Request) {
99 switch r.Method {
100 case http.MethodGet:
101 s.Pages.Notice(w, "settings-emails", "Unimplemented.")
102 log.Println("unimplemented")
103 return
104 case http.MethodPut:
105 did := s.Auth.GetDid(r)
106 emAddr := r.FormValue("email")
107 emAddr = strings.TrimSpace(emAddr)
108
109 if !email.IsValidEmail(emAddr) {
110 s.Pages.Notice(w, "settings-emails-error", "Invalid email address.")
111 return
112 }
113
114 // check if email already exists in database
115 existingEmail, err := db.GetEmail(s.Db, did, emAddr)
116 if err != nil && !errors.Is(err, sql.ErrNoRows) {
117 log.Printf("checking for existing email: %s", err)
118 s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
119 return
120 }
121
122 if err == nil {
123 if existingEmail.Verified {
124 s.Pages.Notice(w, "settings-emails-error", "This email is already verified.")
125 return
126 }
127
128 s.Pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.")
129 return
130 }
131
132 code := uuid.New().String()
133
134 // Begin transaction
135 tx, err := s.Db.Begin()
136 if err != nil {
137 log.Printf("failed to start transaction: %s", err)
138 s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
139 return
140 }
141 defer tx.Rollback()
142
143 if err := db.AddEmail(tx, db.Email{
144 Did: did,
145 Address: emAddr,
146 Verified: false,
147 VerificationCode: code,
148 }); err != nil {
149 log.Printf("adding email: %s", err)
150 s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
151 return
152 }
153
154 if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil {
155 return
156 }
157
158 // Commit transaction
159 if err := tx.Commit(); err != nil {
160 log.Printf("failed to commit transaction: %s", err)
161 s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
162 return
163 }
164
165 s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.")
166 return
167 case http.MethodDelete:
168 did := s.Auth.GetDid(r)
169 emailAddr := r.FormValue("email")
170 emailAddr = strings.TrimSpace(emailAddr)
171
172 // Begin transaction
173 tx, err := s.Db.Begin()
174 if err != nil {
175 log.Printf("failed to start transaction: %s", err)
176 s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
177 return
178 }
179 defer tx.Rollback()
180
181 if err := db.DeleteEmail(tx, did, emailAddr); err != nil {
182 log.Printf("deleting email: %s", err)
183 s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
184 return
185 }
186
187 // Commit transaction
188 if err := tx.Commit(); err != nil {
189 log.Printf("failed to commit transaction: %s", err)
190 s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
191 return
192 }
193
194 s.Pages.HxLocation(w, "/settings")
195 return
196 }
197}
198
199func (s *Settings) verifyUrl(did string, email string, code string) string {
200 var appUrl string
201 if s.Config.Dev {
202 appUrl = "http://" + s.Config.ListenAddr
203 } else {
204 appUrl = "https://tangled.sh"
205 }
206
207 return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code))
208}
209
210func (s *Settings) emailsVerify(w http.ResponseWriter, r *http.Request) {
211 q := r.URL.Query()
212
213 // Get the parameters directly from the query
214 emailAddr := q.Get("email")
215 did := q.Get("did")
216 code := q.Get("code")
217
218 valid, err := db.CheckValidVerificationCode(s.Db, did, emailAddr, code)
219 if err != nil {
220 log.Printf("checking email verification: %s", err)
221 s.Pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.")
222 return
223 }
224
225 if !valid {
226 s.Pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.")
227 return
228 }
229
230 // Mark email as verified in the database
231 if err := db.MarkEmailVerified(s.Db, did, emailAddr); err != nil {
232 log.Printf("marking email as verified: %s", err)
233 s.Pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.")
234 return
235 }
236
237 http.Redirect(w, r, "/settings", http.StatusSeeOther)
238}
239
240func (s *Settings) emailsVerifyResend(w http.ResponseWriter, r *http.Request) {
241 if r.Method != http.MethodPost {
242 s.Pages.Notice(w, "settings-emails-error", "Invalid request method.")
243 return
244 }
245
246 did := s.Auth.GetDid(r)
247 emAddr := r.FormValue("email")
248 emAddr = strings.TrimSpace(emAddr)
249
250 if !email.IsValidEmail(emAddr) {
251 s.Pages.Notice(w, "settings-emails-error", "Invalid email address.")
252 return
253 }
254
255 // Check if email exists and is unverified
256 existingEmail, err := db.GetEmail(s.Db, did, emAddr)
257 if err != nil {
258 if errors.Is(err, sql.ErrNoRows) {
259 s.Pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.")
260 } else {
261 log.Printf("checking for existing email: %s", err)
262 s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
263 }
264 return
265 }
266
267 if existingEmail.Verified {
268 s.Pages.Notice(w, "settings-emails-error", "This email is already verified.")
269 return
270 }
271
272 // Check if last verification email was sent less than 10 minutes ago
273 if existingEmail.LastSent != nil {
274 timeSinceLastSent := time.Since(*existingEmail.LastSent)
275 if timeSinceLastSent < 10*time.Minute {
276 waitTime := 10*time.Minute - timeSinceLastSent
277 s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1)))
278 return
279 }
280 }
281
282 // Generate new verification code
283 code := uuid.New().String()
284
285 // Begin transaction
286 tx, err := s.Db.Begin()
287 if err != nil {
288 log.Printf("failed to start transaction: %s", err)
289 s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
290 return
291 }
292 defer tx.Rollback()
293
294 // Update the verification code and last sent time
295 if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil {
296 log.Printf("updating email verification: %s", err)
297 s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
298 return
299 }
300
301 // Send verification email
302 if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil {
303 return
304 }
305
306 // Commit transaction
307 if err := tx.Commit(); err != nil {
308 log.Printf("failed to commit transaction: %s", err)
309 s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
310 return
311 }
312
313 s.Pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.")
314}
315
316func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) {
317 did := s.Auth.GetDid(r)
318 emailAddr := r.FormValue("email")
319 emailAddr = strings.TrimSpace(emailAddr)
320
321 if emailAddr == "" {
322 s.Pages.Notice(w, "settings-emails-error", "Email address cannot be empty.")
323 return
324 }
325
326 if err := db.MakeEmailPrimary(s.Db, did, emailAddr); err != nil {
327 log.Printf("setting primary email: %s", err)
328 s.Pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.")
329 return
330 }
331
332 s.Pages.HxLocation(w, "/settings")
333}
334
335func (s *Settings) keys(w http.ResponseWriter, r *http.Request) {
336 switch r.Method {
337 case http.MethodGet:
338 s.Pages.Notice(w, "settings-keys", "Unimplemented.")
339 log.Println("unimplemented")
340 return
341 case http.MethodPut:
342 did := s.Auth.GetDid(r)
343 key := r.FormValue("key")
344 key = strings.TrimSpace(key)
345 name := r.FormValue("name")
346 client, _ := s.Auth.AuthorizedClient(r)
347
348 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
349 if err != nil {
350 log.Printf("parsing public key: %s", err)
351 s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.")
352 return
353 }
354
355 rkey := appview.TID()
356
357 tx, err := s.Db.Begin()
358 if err != nil {
359 log.Printf("failed to start tx; adding public key: %s", err)
360 s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
361 return
362 }
363 defer tx.Rollback()
364
365 if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil {
366 log.Printf("adding public key: %s", err)
367 s.Pages.Notice(w, "settings-keys", "Failed to add public key.")
368 return
369 }
370
371 // store in pds too
372 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
373 Collection: tangled.PublicKeyNSID,
374 Repo: did,
375 Rkey: rkey,
376 Record: &lexutil.LexiconTypeDecoder{
377 Val: &tangled.PublicKey{
378 Created: time.Now().Format(time.RFC3339),
379 Key: key,
380 Name: name,
381 }},
382 })
383 // invalid record
384 if err != nil {
385 log.Printf("failed to create record: %s", err)
386 s.Pages.Notice(w, "settings-keys", "Failed to create record.")
387 return
388 }
389
390 log.Println("created atproto record: ", resp.Uri)
391
392 err = tx.Commit()
393 if err != nil {
394 log.Printf("failed to commit tx; adding public key: %s", err)
395 s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
396 return
397 }
398
399 s.Pages.HxLocation(w, "/settings")
400 return
401
402 case http.MethodDelete:
403 did := s.Auth.GetDid(r)
404 q := r.URL.Query()
405
406 name := q.Get("name")
407 rkey := q.Get("rkey")
408 key := q.Get("key")
409
410 log.Println(name)
411 log.Println(rkey)
412 log.Println(key)
413
414 client, _ := s.Auth.AuthorizedClient(r)
415
416 if err := db.RemovePublicKey(s.Db, did, name, key); err != nil {
417 log.Printf("removing public key: %s", err)
418 s.Pages.Notice(w, "settings-keys", "Failed to remove public key.")
419 return
420 }
421
422 if rkey != "" {
423 // remove from pds too
424 _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
425 Collection: tangled.PublicKeyNSID,
426 Repo: did,
427 Rkey: rkey,
428 })
429
430 // invalid record
431 if err != nil {
432 log.Printf("failed to delete record from PDS: %s", err)
433 s.Pages.Notice(w, "settings-keys", "Failed to remove key from PDS.")
434 return
435 }
436 }
437 log.Println("deleted successfully")
438
439 s.Pages.HxLocation(w, "/settings")
440 return
441 }
442}