Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

appview: email: resend verification

+171 -22
+1
appview/db/db.go
··· 202 202 email text not null, 203 203 verified integer not null default 0, 204 204 verification_code text not null, 205 + last_sent text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 205 206 is_primary integer not null default 0, 206 207 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 207 208 unique(did, email)
+51 -6
appview/db/email.go
··· 12 12 Verified bool 13 13 Primary bool 14 14 VerificationCode string 15 + LastSent *time.Time 15 16 CreatedAt time.Time 16 17 } 17 18 18 19 func GetPrimaryEmail(e Execer, did string) (Email, error) { 19 20 query := ` 20 - select id, did, email, verified, is_primary, verification_code, created 21 + select id, did, email, verified, is_primary, verification_code, last_sent, created 21 22 from emails 22 23 where did = ? and is_primary = true 23 24 ` 24 25 var email Email 25 26 var createdStr string 26 - err := e.QueryRow(query, did).Scan(&email.ID, &email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &createdStr) 27 + var lastSent *string 28 + err := e.QueryRow(query, did).Scan(&email.ID, &email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &lastSent, &createdStr) 27 29 if err != nil { 28 30 return Email{}, err 29 31 } 30 32 email.CreatedAt, err = time.Parse(time.RFC3339, createdStr) 31 33 if err != nil { 32 34 return Email{}, err 35 + } 36 + if lastSent != nil { 37 + parsedTime, err := time.Parse(time.RFC3339, *lastSent) 38 + if err != nil { 39 + return Email{}, err 40 + } 41 + email.LastSent = &parsedTime 33 42 } 34 43 return email, nil 35 44 } 36 45 37 46 func GetEmail(e Execer, did string, em string) (Email, error) { 38 47 query := ` 39 - select id, did, email, verified, is_primary, verification_code, created 48 + select id, did, email, verified, is_primary, verification_code, last_sent, created 40 49 from emails 41 50 where did = ? and email = ? 42 51 ` 43 52 var email Email 44 53 var createdStr string 45 - err := e.QueryRow(query, did, em).Scan(&email.ID, &email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &createdStr) 54 + var lastSent *string 55 + err := e.QueryRow(query, did, em).Scan(&email.ID, &email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &lastSent, &createdStr) 46 56 if err != nil { 47 57 return Email{}, err 48 58 } 49 59 email.CreatedAt, err = time.Parse(time.RFC3339, createdStr) 50 60 if err != nil { 51 61 return Email{}, err 62 + } 63 + if lastSent != nil { 64 + parsedTime, err := time.Parse(time.RFC3339, *lastSent) 65 + if err != nil { 66 + return Email{}, err 67 + } 68 + email.LastSent = &parsedTime 52 69 } 53 70 return email, nil 54 71 } ··· 237 220 238 221 func GetAllEmails(e Execer, did string) ([]Email, error) { 239 222 query := ` 240 - select did, email, verified, is_primary, verification_code, created 223 + select did, email, verified, is_primary, verification_code, last_sent, created 241 224 from emails 242 225 where did = ? 243 226 ` ··· 251 234 for rows.Next() { 252 235 var email Email 253 236 var createdStr string 254 - err := rows.Scan(&email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &createdStr) 237 + var lastSent *string 238 + err := rows.Scan(&email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode, &lastSent, &createdStr) 255 239 if err != nil { 256 240 return nil, err 257 241 } ··· 260 242 if err != nil { 261 243 return nil, err 262 244 } 245 + if lastSent != nil { 246 + parsedTime, err := time.Parse(time.RFC3339, *lastSent) 247 + if err != nil { 248 + return nil, err 249 + } 250 + email.LastSent = &parsedTime 251 + } 263 252 emails = append(emails, email) 264 253 } 265 254 return emails, nil 255 + } 256 + 257 + func UpdateVerificationCode(e Execer, did string, email string, code string) error { 258 + query := ` 259 + update emails 260 + set verification_code = ? 261 + where did = ? and email = ? 262 + ` 263 + _, err := e.Exec(query, code, did, email) 264 + return err 265 + } 266 + 267 + func UpdateLastSent(e Execer, did string, email string, lastSent time.Time) error { 268 + query := ` 269 + update emails 270 + set last_sent = ? 271 + where did = ? and email = ? 272 + ` 273 + _, err := e.Exec(query, lastSent.Format(time.RFC3339), did, email) 274 + return err 266 275 }
+11 -1
appview/pages/templates/settings.html
··· 103 103 </div> 104 104 </div> 105 105 <div class="flex gap-2 items-center"> 106 - {{ if not .Primary }} 106 + {{ if not .Verified }} 107 + <a 108 + class="text-sm" 109 + hx-post="/settings/emails/verify/resend" 110 + hx-swap="none" 111 + href="#" 112 + hx-vals='{"email": "{{ .Address }}"}'> 113 + resend verification 114 + </a> 115 + {{ end }} 116 + {{ if and (not .Primary) .Verified }} 107 117 <a 108 118 class="text-sm" 109 119 hx-post="/settings/emails/primary"
+1
appview/state/router.go
··· 170 170 r.Put("/emails", s.SettingsEmails) 171 171 r.Delete("/emails", s.SettingsEmails) 172 172 r.Get("/emails/verify", s.SettingsEmailsVerify) 173 + r.Post("/emails/verify/resend", s.SettingsEmailsVerifyResend) 173 174 r.Post("/emails/primary", s.SettingsEmailsPrimary) 174 175 }) 175 176
+107 -15
appview/state/settings.go
··· 38 38 }) 39 39 } 40 40 41 + // buildVerificationEmail creates an email.Email struct for verification emails 42 + func (s *State) buildVerificationEmail(emailAddr, did, code string) email.Email { 43 + verifyURL := s.verifyUrl(did, emailAddr, code) 44 + 45 + return email.Email{ 46 + APIKey: s.config.ResendApiKey, 47 + From: "noreply@notifs.tangled.sh", 48 + To: emailAddr, 49 + Subject: "Verify your Tangled email", 50 + Text: `Click the link below (or copy and paste it into your browser) to verify your email address. 51 + ` + verifyURL, 52 + Html: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p> 53 + <p><a href="` + verifyURL + `">` + verifyURL + `</a></p>`, 54 + } 55 + } 56 + 57 + // sendVerificationEmail handles the common logic for sending verification emails 58 + func (s *State) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error { 59 + emailToSend := s.buildVerificationEmail(emailAddr, did, code) 60 + 61 + err := email.SendEmail(emailToSend) 62 + if err != nil { 63 + log.Printf("sending email: %s", err) 64 + s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext)) 65 + return err 66 + } 67 + 68 + return nil 69 + } 70 + 41 71 func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) { 42 72 switch r.Method { 43 73 case http.MethodGet: ··· 124 94 return 125 95 } 126 96 127 - err = email.SendEmail(email.Email{ 128 - APIKey: s.config.ResendApiKey, 129 - 130 - From: "noreply@notifs.tangled.sh", 131 - To: emAddr, 132 - Subject: "Verify your Tangled email", 133 - Text: `Click the link below (or copy and paste it into your browser) to verify your email address. 134 - ` + s.verifyUrl(did, emAddr, code), 135 - Html: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p> 136 - <p><a href="` + s.verifyUrl(did, emAddr, code) + `">` + s.verifyUrl(did, emAddr, code) + `</a></p>`, 137 - }) 138 - 139 - if err != nil { 140 - log.Printf("sending email: %s", err) 141 - s.pages.Notice(w, "settings-emails-error", "Unable to send verification email at this moment, try again later.") 97 + if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { 142 98 return 143 99 } 144 100 ··· 208 192 } 209 193 210 194 http.Redirect(w, r, "/settings", http.StatusSeeOther) 195 + } 196 + 197 + func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Request) { 198 + if r.Method != http.MethodPost { 199 + s.pages.Notice(w, "settings-emails-error", "Invalid request method.") 200 + return 201 + } 202 + 203 + did := s.auth.GetDid(r) 204 + emAddr := r.FormValue("email") 205 + emAddr = strings.TrimSpace(emAddr) 206 + 207 + if !email.IsValidEmail(emAddr) { 208 + s.pages.Notice(w, "settings-emails-error", "Invalid email address.") 209 + return 210 + } 211 + 212 + // Check if email exists and is unverified 213 + existingEmail, err := db.GetEmail(s.db, did, emAddr) 214 + if err != nil { 215 + if errors.Is(err, sql.ErrNoRows) { 216 + s.pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.") 217 + } else { 218 + log.Printf("checking for existing email: %s", err) 219 + s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 220 + } 221 + return 222 + } 223 + 224 + if existingEmail.Verified { 225 + s.pages.Notice(w, "settings-emails-error", "This email is already verified.") 226 + return 227 + } 228 + 229 + // Check if last verification email was sent less than 10 minutes ago 230 + if existingEmail.LastSent != nil { 231 + timeSinceLastSent := time.Since(*existingEmail.LastSent) 232 + if timeSinceLastSent < 10*time.Minute { 233 + waitTime := 10*time.Minute - timeSinceLastSent 234 + s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1))) 235 + return 236 + } 237 + } 238 + 239 + // Generate new verification code 240 + code := uuid.New().String() 241 + 242 + // Begin transaction 243 + tx, err := s.db.Begin() 244 + if err != nil { 245 + log.Printf("failed to start transaction: %s", err) 246 + s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 247 + return 248 + } 249 + defer tx.Rollback() 250 + 251 + // Update the verification code and last sent time 252 + if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil { 253 + log.Printf("updating email verification: %s", err) 254 + s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 255 + return 256 + } 257 + 258 + // Send verification email 259 + if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { 260 + return 261 + } 262 + 263 + // Commit transaction 264 + if err := tx.Commit(); err != nil { 265 + log.Printf("failed to commit transaction: %s", err) 266 + s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 267 + return 268 + } 269 + 270 + s.pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.") 211 271 } 212 272 213 273 func (s *State) SettingsEmailsPrimary(w http.ResponseWriter, r *http.Request) {