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