Add student account self-service security features and discoverability fixes; v2.1.0
Adds forgot/reset password, signup email verification, sign-out-of-all-devices, note-usage and certificate display in account settings, and password-changed security emails. Fixes study sessions being lost on server restart by persisting them to disk, and fixes sanitizeStudyUsers silently dropping the new pending-token fields on load. Also surfaces the lesson discussion board in the main site nav, trims the sign-in page for returning users, and renames colossiansStudy.tsx/colossiansStudyData.ts to studyPages.tsx/legacyColossiansData.ts now that the study system covers more than Colossians. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a6e8243705
commit
841e754e16
16 files changed
+727
-50
No files matched your search
+100
@@ -485,6 +485,106 @@ export async function sendStudyWelcomeEmail(email, displayName) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendStudyVerifyEmail(email, displayName, verifyUrl) {
|
||||
if (!process.env.RESEND_API_KEY) return
|
||||
try {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
const cfg = state.cachedSiteContent ?? {}
|
||||
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
|
||||
const subject = cfg.emailVerifySubject?.trim() || 'Verify your email address'
|
||||
const bodyText = cfg.emailVerifyBody?.trim() || 'Please confirm this is your email address so we can keep your study account secure and deliver reminders you opt into.'
|
||||
const ctaLabel = cfg.emailVerifyCtaLabel?.trim() || 'Verify Email Address'
|
||||
const bodyHtml = (
|
||||
`<p style="margin:0 0 16px;">Hi <strong style="color:#f0ead8;">${escapeHtml(namePart)}</strong>,</p>` +
|
||||
`<p style="margin:0 0 16px;">${escapeHtml(bodyText)}</p>`
|
||||
)
|
||||
const { error } = await resend.emails.send({
|
||||
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
|
||||
to: [email],
|
||||
subject,
|
||||
text: `Hi ${namePart},\n\n${bodyText}\n\n${verifyUrl}\n\nVerse by Verse with Nate`,
|
||||
html: buildBrandedEmailHtml({
|
||||
title: 'Verify Your Email',
|
||||
eyebrow: 'Study Account',
|
||||
bodyHtml,
|
||||
ctaLabel,
|
||||
ctaUrl: verifyUrl,
|
||||
footerHtml: `<p style="margin:0;font-family:Georgia,serif;font-size:12px;color:#7a7060;">If you didn't create this account, you can ignore this message.</p>`,
|
||||
}),
|
||||
})
|
||||
if (error) console.error('[study-verify-email] send error:', error)
|
||||
} catch (err) {
|
||||
console.error('[study-verify-email] send exception:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendStudyPasswordResetEmail(email, displayName, resetUrl) {
|
||||
if (!process.env.RESEND_API_KEY) return
|
||||
try {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
const cfg = state.cachedSiteContent ?? {}
|
||||
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
|
||||
const subject = cfg.passwordResetSubject?.trim() || 'Reset your password'
|
||||
const bodyText = cfg.passwordResetBody?.trim() || 'We received a request to reset the password on your study account. Click below to choose a new password. This link expires in 1 hour.'
|
||||
const ctaLabel = cfg.passwordResetCtaLabel?.trim() || 'Reset Password'
|
||||
const bodyHtml = (
|
||||
`<p style="margin:0 0 16px;">Hi <strong style="color:#f0ead8;">${escapeHtml(namePart)}</strong>,</p>` +
|
||||
`<p style="margin:0 0 16px;">${escapeHtml(bodyText)}</p>`
|
||||
)
|
||||
const { error } = await resend.emails.send({
|
||||
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
|
||||
to: [email],
|
||||
subject,
|
||||
text: `Hi ${namePart},\n\n${bodyText}\n\n${resetUrl}\n\nIf you didn't request this, you can safely ignore this email — your password won't change.\n\nVerse by Verse with Nate`,
|
||||
html: buildBrandedEmailHtml({
|
||||
title: 'Reset Your Password',
|
||||
eyebrow: 'Account Security',
|
||||
bodyHtml,
|
||||
ctaLabel,
|
||||
ctaUrl: resetUrl,
|
||||
footerHtml: `<p style="margin:0;font-family:Georgia,serif;font-size:12px;color:#7a7060;">If you didn't request this, you can safely ignore this email — your password won't change.</p>`,
|
||||
}),
|
||||
})
|
||||
if (error) console.error('[study-password-reset] send error:', error)
|
||||
} catch (err) {
|
||||
console.error('[study-password-reset] send exception:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendStudyPasswordChangedEmail(email, displayName) {
|
||||
if (!process.env.RESEND_API_KEY) return
|
||||
try {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
const cfg = state.cachedSiteContent ?? {}
|
||||
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
|
||||
const baseUrl = getCanonicalBaseUrl()
|
||||
const accountUrl = buildAbsoluteUrl(baseUrl, '/study/account')
|
||||
const subject = cfg.passwordChangedSubject?.trim() || 'Your password was changed'
|
||||
const bodyText = cfg.passwordChangedBody?.trim() || 'This confirms the password on your study account was just changed. All other active sessions have been signed out. If this wasn’t you, please contact us right away.'
|
||||
const bodyHtml = (
|
||||
`<p style="margin:0 0 16px;">Hi <strong style="color:#f0ead8;">${escapeHtml(namePart)}</strong>,</p>` +
|
||||
`<p style="margin:0 0 16px;">${escapeHtml(bodyText)}</p>`
|
||||
)
|
||||
const { error } = await resend.emails.send({
|
||||
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
|
||||
to: [email],
|
||||
subject,
|
||||
text: `Hi ${namePart},\n\n${bodyText}\n\nManage your account: ${accountUrl}\n\nVerse by Verse with Nate`,
|
||||
html: buildBrandedEmailHtml({
|
||||
title: 'Password Changed',
|
||||
eyebrow: 'Account Security',
|
||||
bodyHtml,
|
||||
ctaLabel: 'Manage Account',
|
||||
ctaUrl: accountUrl,
|
||||
footerHtml: `<p style="margin:0;font-family:Georgia,serif;font-size:12px;color:#7a7060;">Verse by Verse with Nate</p>`,
|
||||
}),
|
||||
})
|
||||
if (error) console.error('[study-password-changed] send error:', error)
|
||||
} catch (err) {
|
||||
console.error('[study-password-changed] send exception:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmailOtp(email, code) {
|
||||
if (!process.env.RESEND_API_KEY) return
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user