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