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}