Fix email verification: table-based HTML template, URL-encode tokens, remove base64 padding
This commit is contained in:
parent
66fe4bd60e
commit
a87fcb60b6
|
|
@ -395,7 +395,7 @@ func generateRandomString(n int) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -52,39 +53,40 @@ type sendPulseIdentity struct {
|
|||
|
||||
func (s *EmailService) SendVerificationEmail(toEmail, toName, token string) error {
|
||||
subject := "Verify your Sojorn account"
|
||||
// Ensure we don't double up on /api/v1 if it's already in the config
|
||||
apiBase := strings.TrimSuffix(s.config.APIBaseURL, "/api/v1")
|
||||
verifyURL := fmt.Sprintf("%s/api/v1/auth/verify?token=%s", apiBase, token)
|
||||
verifyURL := fmt.Sprintf("%s/api/v1/auth/verify?token=%s", apiBase, url.QueryEscape(token))
|
||||
|
||||
title := "Email Verification"
|
||||
header := fmt.Sprintf("Hey %s! 👋", toName)
|
||||
header := fmt.Sprintf("Hey %s!", toName)
|
||||
if toName == "" {
|
||||
header = "Hey there! 👋"
|
||||
header = "Hey there!"
|
||||
}
|
||||
|
||||
content := `
|
||||
<p>Welcome to Sojorn — your vibrant new social space. We're thrilled to have you join our community!</p>
|
||||
<p>Welcome to Sojorn — your vibrant new social space. We're thrilled to have you join our community!</p>
|
||||
<p>To get started in the app, please verify your email address by clicking the button below:</p>
|
||||
`
|
||||
|
||||
footer := `
|
||||
<div style="background: #F9FAFB; border-radius: 12px; padding: 20px; margin-top: 24px; text-align: left;">
|
||||
<p style="font-size: 13px; color: #9CA3AF; margin-bottom: 8px;">If the button doesn't work, copy and paste this link into your browser:</p>
|
||||
<a href="%s" style="color: #4338CA; text-decoration: underline; word-break: break-all; font-size: 12px; font-weight: 500;">%s</a>
|
||||
</div>
|
||||
<table role="presentation" width="100%%" cellpadding="0" cellspacing="0" style="margin-top: 24px;">
|
||||
<tr><td style="background: #F9FAFB; border-radius: 8px; padding: 16px;">
|
||||
<p style="font-size: 13px; color: #9CA3AF; margin: 0 0 8px 0;">If the button above doesn't work, copy and paste this link into your browser:</p>
|
||||
<a href="%s" style="color: #4338CA; text-decoration: underline; word-break: break-all; font-size: 13px;">%s</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
`
|
||||
footer = fmt.Sprintf(footer, verifyURL, verifyURL)
|
||||
|
||||
htmlBody := s.buildHTMLEmail(title, header, content, verifyURL, "Verify My Email", footer)
|
||||
|
||||
textBody := fmt.Sprintf("Welcome to Sojorn! Please verify your email by clicking here: %s", verifyURL)
|
||||
textBody := fmt.Sprintf("Welcome to Sojorn!\n\nPlease verify your email by visiting this link:\n\n%s\n\nIf you did not create an account, you can ignore this email.", verifyURL)
|
||||
|
||||
return s.sendEmail(toEmail, toName, subject, htmlBody, textBody)
|
||||
}
|
||||
|
||||
func (s *EmailService) SendPasswordResetEmail(toEmail, toName, token string) error {
|
||||
subject := "Reset your Sojorn password"
|
||||
resetURL := fmt.Sprintf("%s/reset-password?token=%s", s.config.AppBaseURL, token)
|
||||
resetURL := fmt.Sprintf("%s/reset-password?token=%s", s.config.AppBaseURL, url.QueryEscape(token))
|
||||
|
||||
title := "Password Reset"
|
||||
header := "Reset your password"
|
||||
|
|
@ -94,11 +96,18 @@ func (s *EmailService) SendPasswordResetEmail(toEmail, toName, token string) err
|
|||
`, toName)
|
||||
|
||||
footer := `
|
||||
<p style="color: #9CA3AF; font-size: 13px; margin-top: 24px;">This link expires in 1 hour. If you did not request this, you can safely ignore this email.</p>
|
||||
<table role="presentation" width="100%%" cellpadding="0" cellspacing="0" style="margin-top: 24px;">
|
||||
<tr><td style="background: #F9FAFB; border-radius: 8px; padding: 16px;">
|
||||
<p style="font-size: 13px; color: #9CA3AF; margin: 0 0 8px 0;">If the button doesn't work, copy and paste this link:</p>
|
||||
<a href="%s" style="color: #4338CA; text-decoration: underline; word-break: break-all; font-size: 13px;">%s</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
<p style="color: #9CA3AF; font-size: 12px; margin-top: 16px;">This link expires in 1 hour. If you did not request this, you can safely ignore this email.</p>
|
||||
`
|
||||
footer = fmt.Sprintf(footer, resetURL, resetURL)
|
||||
|
||||
htmlBody := s.buildHTMLEmail(title, header, content, resetURL, "Reset Password", footer)
|
||||
textBody := fmt.Sprintf("Reset your Sojorn password: %s", resetURL)
|
||||
textBody := fmt.Sprintf("Reset your Sojorn password by visiting this link:\n\n%s\n\nThis link expires in 1 hour.", resetURL)
|
||||
|
||||
return s.sendEmail(toEmail, toName, subject, htmlBody, textBody)
|
||||
}
|
||||
|
|
@ -257,49 +266,64 @@ func (s *EmailService) AddSubscriber(email, name string) {
|
|||
}
|
||||
|
||||
func (s *EmailService) buildHTMLEmail(title, header, content, buttonURL, buttonText, footer string) string {
|
||||
return fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
return fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<title>%s</title>
|
||||
<!--[if mso]>
|
||||
<noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript>
|
||||
<![endif]-->
|
||||
<style>
|
||||
body, table, td, a { -webkit-text-size-adjust: 100%%; -ms-text-size-adjust: 100%%; }
|
||||
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%%; outline: none; text-decoration: none; }
|
||||
body { margin: 0; padding: 0; width: 100%% !important; }
|
||||
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; background-color: #F3F4F6;">
|
||||
<div style="padding: 40px 20px; background-color: #F3F4F6;">
|
||||
<div style="max-width: 520px; margin: 0 auto; background-color: #ffffff; border-radius: 24px; overflow: hidden; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);">
|
||||
<!-- Header -->
|
||||
<div style="background: linear-gradient(135deg, #4338CA 0%%, #6366F1 100%%); padding: 40px; text-align: center;">
|
||||
<img src="https://sojorn.net/web.png" alt="Sojorn" style="width: 80px; height: 80px; border-radius: 20px; margin-bottom: 16px;">
|
||||
<div style="color: #ffffff; font-size: 12px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; opacity: 0.9;">%s</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div style="padding: 40px; text-align: center; color: #374151;">
|
||||
<h1 style="color: #1F2937; font-size: 24px; font-weight: 700; margin-bottom: 16px;">%s</h1>
|
||||
<div style="font-size: 16px; line-height: 1.6; color: #4B5563; margin-bottom: 32px;">
|
||||
%s
|
||||
</div>
|
||||
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; background-color: #F3F4F6; width: 100%%;">
|
||||
<table role="presentation" width="100%%" cellpadding="0" cellspacing="0" style="background-color: #F3F4F6;">
|
||||
<tr><td style="padding: 40px 20px;">
|
||||
<table role="presentation" width="520" cellpadding="0" cellspacing="0" align="center" style="max-width: 520px; margin: 0 auto; background-color: #ffffff; border-radius: 16px; overflow: hidden;">
|
||||
<!-- Header -->
|
||||
<tr><td style="background-color: #4338CA; padding: 40px; text-align: center;">
|
||||
<img src="https://sojorn.net/web.png" alt="Sojorn" width="80" height="80" style="width: 80px; height: 80px; border-radius: 20px; margin-bottom: 16px; display: block; margin-left: auto; margin-right: auto;">
|
||||
<p style="color: #ffffff; font-size: 12px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; margin: 0;">%s</p>
|
||||
</td></tr>
|
||||
|
||||
<a href="%s" style="display: inline-block; padding: 16px 40px; background-color: #4338CA; color: #ffffff; text-decoration: none; border-radius: 12px; font-weight: 600; font-size: 16px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
|
||||
<!-- Content -->
|
||||
<tr><td style="padding: 40px; text-align: center; color: #374151;">
|
||||
<h1 style="color: #1F2937; font-size: 24px; font-weight: 700; margin: 0 0 16px 0;">%s</h1>
|
||||
<div style="font-size: 16px; line-height: 1.6; color: #4B5563; margin-bottom: 32px; text-align: left;">
|
||||
%s
|
||||
</div>
|
||||
|
||||
<!-- Button (table-based for Outlook) -->
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" align="center" style="margin: 0 auto;">
|
||||
<tr><td style="background-color: #4338CA; border-radius: 12px; text-align: center;">
|
||||
<a href="%s" target="_blank" style="display: block; padding: 16px 40px; color: #ffffff; text-decoration: none; font-weight: 600; font-size: 16px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">%s</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
%s
|
||||
</a>
|
||||
</td></tr>
|
||||
|
||||
%s
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="padding: 32px; text-align: center; background-color: #F9FAFB; border-top: 1px solid #E5E7EB;">
|
||||
<p style="font-size: 12px; color: #9CA3AF; margin-bottom: 8px;">© 2026 Sojorn by MPLS LLC. All rights reserved.</p>
|
||||
<div style="font-size: 12px; color: #9CA3AF;">
|
||||
<a href="https://sojorn.net" style="color: #9CA3AF; text-decoration: none; margin: 0 8px;">Website</a> •
|
||||
<a href="https://sojorn.net/privacy" style="color: #9CA3AF; text-decoration: none; margin: 0 8px;">Privacy</a> •
|
||||
<a href="https://sojorn.net/terms" style="color: #9CA3AF; text-decoration: none; margin: 0 8px;">Terms</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<tr><td style="padding: 32px; text-align: center; background-color: #F9FAFB; border-top: 1px solid #E5E7EB;">
|
||||
<p style="font-size: 12px; color: #9CA3AF; margin: 0 0 8px 0;">© 2026 Sojorn by MPLS LLC. All rights reserved.</p>
|
||||
<p style="font-size: 12px; color: #9CA3AF; margin: 0;">
|
||||
<a href="https://sojorn.net" style="color: #9CA3AF; text-decoration: none;">Website</a> •
|
||||
<a href="https://sojorn.net/privacy" style="color: #9CA3AF; text-decoration: none;">Privacy</a> •
|
||||
<a href="https://sojorn.net/terms" style="color: #9CA3AF; text-decoration: none;">Terms</a>
|
||||
</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`, title, title, header, content, buttonURL, buttonText, footer)
|
||||
</html>`, title, title, header, content, buttonURL, buttonText, footer)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue