From 3edc2a73ba936989e344fdab5be18b796dd7c1d2 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Sat, 22 Mar 2025 15:00:39 +0200 Subject: [PATCH] appview: email: resend verification --- appview/db/db.go | 1 + appview/db/email.go | 57 ++++++++++-- appview/pages/templates/settings.html | 12 ++- appview/state/router.go | 1 + appview/state/settings.go | 122 ++++++++++++++++++++++---- 5 files changed, 171 insertions(+), 22 deletions(-) diff --git a/appview/db/db.go b/appview/db/db.go index b2147576..88b0b60b 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -202,6 +202,7 @@ func Make(dbPath string) (*DB, error) { email text not null, verified integer not null default 0, verification_code text not null, + last_sent text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), is_primary integer not null default 0, created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), unique(did, email) diff --git a/appview/db/email.go b/appview/db/email.go index d32c6b28..d69956c8 100644 --- a/appview/db/email.go +++ b/appview/db/email.go @@ -12,18 +12,20 @@ type Email struct { Verified bool Primary bool VerificationCode string + LastSent *time.Time CreatedAt time.Time } func GetPrimaryEmail(e Execer, did string) (Email, error) { query := ` - select id, did, email, verified, is_primary, verification_code, created + select id, did, email, verified, is_primary, verification_code, last_sent, created from emails where did = ? and is_primary = true ` var email Email var createdStr string - err := e.QueryRow(query, did).Scan(&email.ID, &email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &createdStr) + var lastSent *string + err := e.QueryRow(query, did).Scan(&email.ID, &email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &lastSent, &createdStr) if err != nil { return Email{}, err } @@ -31,18 +33,26 @@ func GetPrimaryEmail(e Execer, did string) (Email, error) { if err != nil { return Email{}, err } + if lastSent != nil { + parsedTime, err := time.Parse(time.RFC3339, *lastSent) + if err != nil { + return Email{}, err + } + email.LastSent = &parsedTime + } return email, nil } func GetEmail(e Execer, did string, em string) (Email, error) { query := ` - select id, did, email, verified, is_primary, verification_code, created + select id, did, email, verified, is_primary, verification_code, last_sent, created from emails where did = ? and email = ? ` var email Email var createdStr string - err := e.QueryRow(query, did, em).Scan(&email.ID, &email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &createdStr) + var lastSent *string + err := e.QueryRow(query, did, em).Scan(&email.ID, &email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &lastSent, &createdStr) if err != nil { return Email{}, err } @@ -50,6 +60,13 @@ func GetEmail(e Execer, did string, em string) (Email, error) { if err != nil { return Email{}, err } + if lastSent != nil { + parsedTime, err := time.Parse(time.RFC3339, *lastSent) + if err != nil { + return Email{}, err + } + email.LastSent = &parsedTime + } return email, nil } @@ -220,7 +237,7 @@ func MakeEmailPrimary(e Execer, did string, email string) error { func GetAllEmails(e Execer, did string) ([]Email, error) { query := ` - select did, email, verified, is_primary, verification_code, created + select did, email, verified, is_primary, verification_code, last_sent, created from emails where did = ? ` @@ -234,7 +251,8 @@ func GetAllEmails(e Execer, did string) ([]Email, error) { for rows.Next() { var email Email var createdStr string - err := rows.Scan(&email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &createdStr) + var lastSent *string + err := rows.Scan(&email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &lastSent, &createdStr) if err != nil { return nil, err } @@ -242,7 +260,34 @@ func GetAllEmails(e Execer, did string) ([]Email, error) { if err != nil { return nil, err } + if lastSent != nil { + parsedTime, err := time.Parse(time.RFC3339, *lastSent) + if err != nil { + return nil, err + } + email.LastSent = &parsedTime + } emails = append(emails, email) } return emails, nil } + +func UpdateVerificationCode(e Execer, did string, email string, code string) error { + query := ` + update emails + set verification_code = ? + where did = ? and email = ? + ` + _, err := e.Exec(query, code, did, email) + return err +} + +func UpdateLastSent(e Execer, did string, email string, lastSent time.Time) error { + query := ` + update emails + set last_sent = ? + where did = ? and email = ? + ` + _, err := e.Exec(query, lastSent.Format(time.RFC3339), did, email) + return err +} diff --git a/appview/pages/templates/settings.html b/appview/pages/templates/settings.html index b895a655..cc814b8a 100644 --- a/appview/pages/templates/settings.html +++ b/appview/pages/templates/settings.html @@ -103,7 +103,17 @@
- {{ if not .Primary }} + {{ if not .Verified }} + + resend verification + + {{ end }} + {{ if and (not .Primary) .Verified }} Click the link (or copy and paste it into your browser) to verify your email address.

+

` + verifyURL + `

`, + } +} + +// sendVerificationEmail handles the common logic for sending verification emails +func (s *State) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error { + emailToSend := s.buildVerificationEmail(emailAddr, did, code) + + err := email.SendEmail(emailToSend) + if err != nil { + log.Printf("sending email: %s", err) + s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext)) + return err + } + + return nil +} + func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: @@ -94,21 +124,7 @@ func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) { return } - err = email.SendEmail(email.Email{ - APIKey: s.config.ResendApiKey, - - From: "noreply@notifs.tangled.sh", - To: emAddr, - Subject: "Verify your Tangled email", - Text: `Click the link below (or copy and paste it into your browser) to verify your email address. -` + s.verifyUrl(did, emAddr, code), - Html: `

Click the link (or copy and paste it into your browser) to verify your email address.

-

` + s.verifyUrl(did, emAddr, code) + `

`, - }) - - if err != nil { - log.Printf("sending email: %s", err) - s.pages.Notice(w, "settings-emails-error", "Unable to send verification email at this moment, try again later.") + if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { return } @@ -194,6 +210,82 @@ func (s *State) SettingsEmailsVerify(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/settings", http.StatusSeeOther) } +func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + s.pages.Notice(w, "settings-emails-error", "Invalid request method.") + return + } + + did := s.auth.GetDid(r) + emAddr := r.FormValue("email") + emAddr = strings.TrimSpace(emAddr) + + if !email.IsValidEmail(emAddr) { + s.pages.Notice(w, "settings-emails-error", "Invalid email address.") + return + } + + // Check if email exists and is unverified + existingEmail, err := db.GetEmail(s.db, did, emAddr) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + s.pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.") + } else { + log.Printf("checking for existing email: %s", err) + s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") + } + return + } + + if existingEmail.Verified { + s.pages.Notice(w, "settings-emails-error", "This email is already verified.") + return + } + + // Check if last verification email was sent less than 10 minutes ago + if existingEmail.LastSent != nil { + timeSinceLastSent := time.Since(*existingEmail.LastSent) + if timeSinceLastSent < 10*time.Minute { + waitTime := 10*time.Minute - timeSinceLastSent + s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1))) + return + } + } + + // Generate new verification code + code := uuid.New().String() + + // Begin transaction + tx, err := s.db.Begin() + if err != nil { + log.Printf("failed to start transaction: %s", err) + s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") + return + } + defer tx.Rollback() + + // Update the verification code and last sent time + if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil { + log.Printf("updating email verification: %s", err) + s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") + return + } + + // Send verification email + if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { + return + } + + // Commit transaction + if err := tx.Commit(); err != nil { + log.Printf("failed to commit transaction: %s", err) + s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") + return + } + + s.pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.") +} + func (s *State) SettingsEmailsPrimary(w http.ResponseWriter, r *http.Request) { did := s.auth.GetDid(r) emailAddr := r.FormValue("email") -- 2.51.2