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