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.org/core/api/tangled"
15 "tangled.org/core/appview/config"
16 "tangled.org/core/appview/db"
17 "tangled.org/core/appview/email"
18 "tangled.org/core/appview/middleware"
19 "tangled.org/core/appview/models"
20 "tangled.org/core/appview/oauth"
21 "tangled.org/core/appview/pages"
22 "tangled.org/core/tid"
23
24 comatproto "github.com/bluesky-social/indigo/api/atproto"
25 "github.com/bluesky-social/indigo/atproto/syntax"
26 lexutil "github.com/bluesky-social/indigo/lex/util"
27 "github.com/gliderlabs/ssh"
28 "github.com/google/uuid"
29)
30
31type Settings struct {
32 Db *db.DB
33 OAuth *oauth.OAuth
34 Pages *pages.Pages
35 Config *config.Config
36}
37
38func (s *Settings) Router() http.Handler {
39 r := chi.NewRouter()
40
41 r.Use(middleware.AuthMiddleware(s.OAuth))
42
43 // settings pages
44 r.Get("/", s.profileSettings)
45 r.Get("/profile", s.profileSettings)
46
47 r.Route("/keys", func(r chi.Router) {
48 r.Get("/", s.keysSettings)
49 r.Put("/", s.keys)
50 r.Delete("/", s.keys)
51 })
52
53 r.Route("/emails", func(r chi.Router) {
54 r.Get("/", s.emailsSettings)
55 r.Put("/", s.emails)
56 r.Delete("/", s.emails)
57 r.Get("/verify", s.emailsVerify)
58 r.Post("/verify/resend", s.emailsVerifyResend)
59 r.Post("/primary", s.emailsPrimary)
60 })
61
62 r.Route("/notifications", func(r chi.Router) {
63 r.Get("/", s.notificationsSettings)
64 r.Put("/", s.updateNotificationPreferences)
65 })
66
67 return r
68}
69
70func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) {
71 user := s.OAuth.GetUser(r)
72
73 s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{
74 LoggedInUser: user,
75 Tab: "profile",
76 })
77}
78
79func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) {
80 user := s.OAuth.GetUser(r)
81 did := s.OAuth.GetDid(r)
82
83 prefs, err := db.GetNotificationPreference(s.Db, did)
84 if err != nil {
85 log.Printf("failed to get notification preferences: %s", err)
86 s.Pages.Notice(w, "settings-notifications-error", "Unable to load notification preferences.")
87 return
88 }
89
90 s.Pages.UserNotificationSettings(w, pages.UserNotificationSettingsParams{
91 LoggedInUser: user,
92 Preferences: prefs,
93 Tab: "notifications",
94 })
95}
96
97func (s *Settings) updateNotificationPreferences(w http.ResponseWriter, r *http.Request) {
98 did := s.OAuth.GetDid(r)
99
100 prefs := &models.NotificationPreferences{
101 UserDid: syntax.DID(did),
102 RepoStarred: r.FormValue("repo_starred") == "on",
103 IssueCreated: r.FormValue("issue_created") == "on",
104 IssueCommented: r.FormValue("issue_commented") == "on",
105 IssueClosed: r.FormValue("issue_closed") == "on",
106 PullCreated: r.FormValue("pull_created") == "on",
107 PullCommented: r.FormValue("pull_commented") == "on",
108 PullMerged: r.FormValue("pull_merged") == "on",
109 Followed: r.FormValue("followed") == "on",
110 UserMentioned: r.FormValue("user_mentioned") == "on",
111 EmailNotifications: r.FormValue("email_notifications") == "on",
112 }
113
114 err := s.Db.UpdateNotificationPreferences(r.Context(), prefs)
115 if err != nil {
116 log.Printf("failed to update notification preferences: %s", err)
117 s.Pages.Notice(w, "settings-notifications-error", "Unable to save notification preferences.")
118 return
119 }
120
121 s.Pages.Notice(w, "settings-notifications-success", "Notification preferences saved successfully.")
122}
123
124func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) {
125 user := s.OAuth.GetUser(r)
126 pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Did)
127 if err != nil {
128 log.Println(err)
129 }
130
131 s.Pages.UserKeysSettings(w, pages.UserKeysSettingsParams{
132 LoggedInUser: user,
133 PubKeys: pubKeys,
134 Tab: "keys",
135 })
136}
137
138func (s *Settings) emailsSettings(w http.ResponseWriter, r *http.Request) {
139 user := s.OAuth.GetUser(r)
140 emails, err := db.GetAllEmails(s.Db, user.Did)
141 if err != nil {
142 log.Println(err)
143 }
144
145 s.Pages.UserEmailsSettings(w, pages.UserEmailsSettingsParams{
146 LoggedInUser: user,
147 Emails: emails,
148 Tab: "emails",
149 })
150}
151
152// buildVerificationEmail creates an email.Email struct for verification emails
153func (s *Settings) buildVerificationEmail(emailAddr, did, code string) email.Email {
154 verifyURL := s.verifyUrl(did, emailAddr, code)
155
156 return email.Email{
157 APIKey: s.Config.Resend.ApiKey,
158 From: s.Config.Resend.SentFrom,
159 To: emailAddr,
160 Subject: "Verify your Tangled email",
161 Text: `Click the link below (or copy and paste it into your browser) to verify your email address.
162` + verifyURL,
163 Html: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p>
164<p><a href="` + verifyURL + `">` + verifyURL + `</a></p>`,
165 }
166}
167
168// sendVerificationEmail handles the common logic for sending verification emails
169func (s *Settings) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error {
170 emailToSend := s.buildVerificationEmail(emailAddr, did, code)
171
172 err := email.SendEmail(emailToSend)
173 if err != nil {
174 log.Printf("sending email: %s", err)
175 s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext))
176 return err
177 }
178
179 return nil
180}
181
182func (s *Settings) emails(w http.ResponseWriter, r *http.Request) {
183 switch r.Method {
184 case http.MethodGet:
185 s.Pages.Notice(w, "settings-emails", "Unimplemented.")
186 log.Println("unimplemented")
187 return
188 case http.MethodPut:
189 did := s.OAuth.GetDid(r)
190 emAddr := r.FormValue("email")
191 emAddr = strings.TrimSpace(emAddr)
192
193 if !email.IsValidEmail(emAddr) {
194 s.Pages.Notice(w, "settings-emails-error", "Invalid email address.")
195 return
196 }
197
198 // check if email already exists in database
199 existingEmail, err := db.GetEmail(s.Db, did, emAddr)
200 if err != nil && !errors.Is(err, sql.ErrNoRows) {
201 log.Printf("checking for existing email: %s", err)
202 s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
203 return
204 }
205
206 if err == nil {
207 if existingEmail.Verified {
208 s.Pages.Notice(w, "settings-emails-error", "This email is already verified.")
209 return
210 }
211
212 s.Pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.")
213 return
214 }
215
216 code := uuid.New().String()
217
218 // Begin transaction
219 tx, err := s.Db.Begin()
220 if err != nil {
221 log.Printf("failed to start transaction: %s", err)
222 s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
223 return
224 }
225 defer tx.Rollback()
226
227 if err := db.AddEmail(tx, models.Email{
228 Did: did,
229 Address: emAddr,
230 Verified: false,
231 VerificationCode: code,
232 }); err != nil {
233 log.Printf("adding email: %s", err)
234 s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
235 return
236 }
237
238 if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil {
239 return
240 }
241
242 // Commit transaction
243 if err := tx.Commit(); err != nil {
244 log.Printf("failed to commit transaction: %s", err)
245 s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
246 return
247 }
248
249 s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.")
250 return
251 case http.MethodDelete:
252 did := s.OAuth.GetDid(r)
253 emailAddr := r.FormValue("email")
254 emailAddr = strings.TrimSpace(emailAddr)
255
256 // Begin transaction
257 tx, err := s.Db.Begin()
258 if err != nil {
259 log.Printf("failed to start transaction: %s", err)
260 s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
261 return
262 }
263 defer tx.Rollback()
264
265 if err := db.DeleteEmail(tx, did, emailAddr); err != nil {
266 log.Printf("deleting email: %s", err)
267 s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
268 return
269 }
270
271 // Commit transaction
272 if err := tx.Commit(); err != nil {
273 log.Printf("failed to commit transaction: %s", err)
274 s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
275 return
276 }
277
278 s.Pages.HxLocation(w, "/settings/emails")
279 return
280 }
281}
282
283func (s *Settings) verifyUrl(did string, email string, code string) string {
284 var appUrl string
285 if s.Config.Core.Dev {
286 appUrl = "http://" + s.Config.Core.ListenAddr
287 } else {
288 appUrl = s.Config.Core.AppviewHost
289 }
290
291 return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code))
292}
293
294func (s *Settings) emailsVerify(w http.ResponseWriter, r *http.Request) {
295 q := r.URL.Query()
296
297 // Get the parameters directly from the query
298 emailAddr := q.Get("email")
299 did := q.Get("did")
300 code := q.Get("code")
301
302 valid, err := db.CheckValidVerificationCode(s.Db, did, emailAddr, code)
303 if err != nil {
304 log.Printf("checking email verification: %s", err)
305 s.Pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.")
306 return
307 }
308
309 if !valid {
310 s.Pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.")
311 return
312 }
313
314 // Mark email as verified in the database
315 if err := db.MarkEmailVerified(s.Db, did, emailAddr); err != nil {
316 log.Printf("marking email as verified: %s", err)
317 s.Pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.")
318 return
319 }
320
321 http.Redirect(w, r, "/settings/emails", http.StatusSeeOther)
322}
323
324func (s *Settings) emailsVerifyResend(w http.ResponseWriter, r *http.Request) {
325 if r.Method != http.MethodPost {
326 s.Pages.Notice(w, "settings-emails-error", "Invalid request method.")
327 return
328 }
329
330 did := s.OAuth.GetDid(r)
331 emAddr := r.FormValue("email")
332 emAddr = strings.TrimSpace(emAddr)
333
334 if !email.IsValidEmail(emAddr) {
335 s.Pages.Notice(w, "settings-emails-error", "Invalid email address.")
336 return
337 }
338
339 // Check if email exists and is unverified
340 existingEmail, err := db.GetEmail(s.Db, did, emAddr)
341 if err != nil {
342 if errors.Is(err, sql.ErrNoRows) {
343 s.Pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.")
344 } else {
345 log.Printf("checking for existing email: %s", err)
346 s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
347 }
348 return
349 }
350
351 if existingEmail.Verified {
352 s.Pages.Notice(w, "settings-emails-error", "This email is already verified.")
353 return
354 }
355
356 // Check if last verification email was sent less than 10 minutes ago
357 if existingEmail.LastSent != nil {
358 timeSinceLastSent := time.Since(*existingEmail.LastSent)
359 if timeSinceLastSent < 10*time.Minute {
360 waitTime := 10*time.Minute - timeSinceLastSent
361 s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1)))
362 return
363 }
364 }
365
366 // Generate new verification code
367 code := uuid.New().String()
368
369 // Begin transaction
370 tx, err := s.Db.Begin()
371 if err != nil {
372 log.Printf("failed to start transaction: %s", err)
373 s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
374 return
375 }
376 defer tx.Rollback()
377
378 // Update the verification code and last sent time
379 if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil {
380 log.Printf("updating email verification: %s", err)
381 s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
382 return
383 }
384
385 // Send verification email
386 if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil {
387 return
388 }
389
390 // Commit transaction
391 if err := tx.Commit(); err != nil {
392 log.Printf("failed to commit transaction: %s", err)
393 s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.")
394 return
395 }
396
397 s.Pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.")
398}
399
400func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) {
401 did := s.OAuth.GetDid(r)
402 emailAddr := r.FormValue("email")
403 emailAddr = strings.TrimSpace(emailAddr)
404
405 if emailAddr == "" {
406 s.Pages.Notice(w, "settings-emails-error", "Email address cannot be empty.")
407 return
408 }
409
410 if err := db.MakeEmailPrimary(s.Db, did, emailAddr); err != nil {
411 log.Printf("setting primary email: %s", err)
412 s.Pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.")
413 return
414 }
415
416 s.Pages.HxLocation(w, "/settings/emails")
417}
418
419func (s *Settings) keys(w http.ResponseWriter, r *http.Request) {
420 switch r.Method {
421 case http.MethodGet:
422 s.Pages.Notice(w, "settings-keys", "Unimplemented.")
423 log.Println("unimplemented")
424 return
425 case http.MethodPut:
426 did := s.OAuth.GetDid(r)
427 key := r.FormValue("key")
428 key = strings.TrimSpace(key)
429 name := r.FormValue("name")
430 client, err := s.OAuth.AuthorizedClient(r)
431 if err != nil {
432 s.Pages.Notice(w, "settings-keys", "Failed to authorize. Try again later.")
433 return
434 }
435
436 _, _, _, _, err = ssh.ParseAuthorizedKey([]byte(key))
437 if err != nil {
438 log.Printf("parsing public key: %s", err)
439 s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.")
440 return
441 }
442
443 rkey := tid.TID()
444
445 tx, err := s.Db.Begin()
446 if err != nil {
447 log.Printf("failed to start tx; adding public key: %s", err)
448 s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
449 return
450 }
451 defer tx.Rollback()
452
453 if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil {
454 log.Printf("adding public key: %s", err)
455 s.Pages.Notice(w, "settings-keys", "Failed to add public key.")
456 return
457 }
458
459 // store in pds too
460 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
461 Collection: tangled.PublicKeyNSID,
462 Repo: did,
463 Rkey: rkey,
464 Record: &lexutil.LexiconTypeDecoder{
465 Val: &tangled.PublicKey{
466 CreatedAt: time.Now().Format(time.RFC3339),
467 Key: key,
468 Name: name,
469 }},
470 })
471 // invalid record
472 if err != nil {
473 log.Printf("failed to create record: %s", err)
474 s.Pages.Notice(w, "settings-keys", "Failed to create record.")
475 return
476 }
477
478 log.Println("created atproto record: ", resp.Uri)
479
480 err = tx.Commit()
481 if err != nil {
482 log.Printf("failed to commit tx; adding public key: %s", err)
483 s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
484 return
485 }
486
487 s.Pages.HxLocation(w, "/settings/keys")
488 return
489
490 case http.MethodDelete:
491 did := s.OAuth.GetDid(r)
492 q := r.URL.Query()
493
494 name := q.Get("name")
495 rkey := q.Get("rkey")
496 key := q.Get("key")
497
498 log.Println(name)
499 log.Println(rkey)
500 log.Println(key)
501
502 client, err := s.OAuth.AuthorizedClient(r)
503 if err != nil {
504 log.Printf("failed to authorize client: %s", err)
505 s.Pages.Notice(w, "settings-keys", "Failed to authorize client.")
506 return
507 }
508
509 if err := db.DeletePublicKey(s.Db, did, name, key); err != nil {
510 log.Printf("removing public key: %s", err)
511 s.Pages.Notice(w, "settings-keys", "Failed to remove public key.")
512 return
513 }
514
515 if rkey != "" {
516 // remove from pds too
517 _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
518 Collection: tangled.PublicKeyNSID,
519 Repo: did,
520 Rkey: rkey,
521 })
522
523 // invalid record
524 if err != nil {
525 log.Printf("failed to delete record from PDS: %s", err)
526 s.Pages.Notice(w, "settings-keys", "Failed to remove key from PDS.")
527 return
528 }
529 }
530 log.Println("deleted successfully")
531
532 s.Pages.HxLocation(w, "/settings/keys")
533 return
534 }
535}