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