forked from tangled.org/core
this repo has no description

appview: email: resend verification

anirudh.fi 8f5bcee3 b120a6e4

verified
Changed files
+171 -22
appview
db
pages
templates
state
+1
appview/db/db.go
···
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)
+51 -6
appview/db/email.go
···
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
}
email.CreatedAt, err = time.Parse(time.RFC3339, createdStr)
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
}
email.CreatedAt, err = time.Parse(time.RFC3339, createdStr)
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 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 = ?
`
···
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
}
···
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
+
}
+11 -1
appview/pages/templates/settings.html
···
</div>
</div>
<div class="flex gap-2 items-center">
-
{{ if not .Primary }}
+
{{ if not .Verified }}
+
<a
+
class="text-sm"
+
hx-post="/settings/emails/verify/resend"
+
hx-swap="none"
+
href="#"
+
hx-vals='{"email": "{{ .Address }}"}'>
+
resend verification
+
</a>
+
{{ end }}
+
{{ if and (not .Primary) .Verified }}
<a
class="text-sm"
hx-post="/settings/emails/primary"
+1
appview/state/router.go
···
r.Put("/emails", s.SettingsEmails)
r.Delete("/emails", s.SettingsEmails)
r.Get("/emails/verify", s.SettingsEmailsVerify)
+
r.Post("/emails/verify/resend", s.SettingsEmailsVerifyResend)
r.Post("/emails/primary", s.SettingsEmailsPrimary)
})
+107 -15
appview/state/settings.go
···
})
}
+
// buildVerificationEmail creates an email.Email struct for verification emails
+
func (s *State) buildVerificationEmail(emailAddr, did, code string) email.Email {
+
verifyURL := s.verifyUrl(did, emailAddr, code)
+
+
return email.Email{
+
APIKey: s.config.ResendApiKey,
+
From: "noreply@notifs.tangled.sh",
+
To: emailAddr,
+
Subject: "Verify your Tangled email",
+
Text: `Click the link below (or copy and paste it into your browser) to verify your email address.
+
` + verifyURL,
+
Html: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p>
+
<p><a href="` + verifyURL + `">` + verifyURL + `</a></p>`,
+
}
+
}
+
+
// 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:
···
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: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p>
-
<p><a href="` + s.verifyUrl(did, emAddr, code) + `">` + s.verifyUrl(did, emAddr, code) + `</a></p>`,
-
})
-
-
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
}
···
}
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) {