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
@@ -3,8 +3,8 @@ import rateLimit from 'express-rate-limit'
|
||||
import { Document, Packer, Paragraph, HeadingLevel, TextRun } from 'docx'
|
||||
import { mkdir, unlink, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { inferImageExtensionFromDataUrl, normalizeAssetBaseName, escapeHtml, buildAbsoluteUrl } from '../helpers.js'
|
||||
import { UPLOADS_DIR, EMAIL_CHANGE_TOKEN_TTL_MS } from '../config.js'
|
||||
import { inferImageExtensionFromDataUrl, normalizeAssetBaseName, escapeHtml, buildAbsoluteUrl, parseCookies } from '../helpers.js'
|
||||
import { UPLOADS_DIR, EMAIL_CHANGE_TOKEN_TTL_MS, MAX_STUDY_NOTES_PER_USER, MAX_STUDY_NOTE_LENGTH, STUDY_SESSION_COOKIE } from '../config.js'
|
||||
import { state } from '../state.js'
|
||||
import {
|
||||
queueStudyUsersWrite,
|
||||
@@ -30,9 +30,11 @@ import {
|
||||
normalizeStudySlug,
|
||||
getStudyCatalog,
|
||||
clearStudySessionCookie,
|
||||
invalidateAllStudySessions,
|
||||
} from '../study-helpers.js'
|
||||
import {
|
||||
sendStudyAccountDeletedEmail,
|
||||
sendStudyPasswordChangedEmail,
|
||||
syncContactToResend,
|
||||
buildBrandedEmailHtml,
|
||||
getCanonicalBaseUrl,
|
||||
@@ -187,6 +189,11 @@ export function register(app) {
|
||||
user.passwordHash = hashStudyPassword(newPassword)
|
||||
user.updatedAt = new Date().toISOString()
|
||||
queueStudyUsersWrite()
|
||||
invalidateAllStudySessions(user.id, parseCookies(req.headers.cookie)[STUDY_SESSION_COOKIE])
|
||||
|
||||
sendStudyPasswordChangedEmail(user.username, user.displayName || user.username)
|
||||
.catch(err => console.error('[study-account] password-changed email error:', err))
|
||||
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
@@ -213,6 +220,16 @@ export function register(app) {
|
||||
}
|
||||
})
|
||||
|
||||
const baseUrl = getCanonicalBaseUrl()
|
||||
const certificates = state.studyCertificates
|
||||
.filter(c => c.userId === user.id)
|
||||
.map(c => ({
|
||||
studySlug: c.studySlug,
|
||||
studyTitle: c.studyTitle,
|
||||
issuedAt: c.issuedAt,
|
||||
url: buildAbsoluteUrl(baseUrl, `/study/certificate/${c.token}`),
|
||||
}))
|
||||
|
||||
res.json({
|
||||
profile: {
|
||||
username: user.username,
|
||||
@@ -222,9 +239,12 @@ export function register(app) {
|
||||
reminderFrequency: user.reminderFrequency ?? 'immediate',
|
||||
remindersPausedUntil: user.remindersPausedUntil ?? null,
|
||||
avatarUrl: getStudyAvatarUrl(user),
|
||||
emailVerified: user.emailVerified === true,
|
||||
},
|
||||
stats: {
|
||||
noteCount: Object.keys(notes).length,
|
||||
noteLimit: MAX_STUDY_NOTES_PER_USER,
|
||||
noteCharLimit: MAX_STUDY_NOTE_LENGTH,
|
||||
memberSince: user.createdAt,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
currentStreak: user.currentStreak ?? 0,
|
||||
@@ -232,6 +252,7 @@ export function register(app) {
|
||||
lastStudiedDate: user.lastStudiedDate ?? null,
|
||||
},
|
||||
studies,
|
||||
certificates,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -478,9 +499,7 @@ export function register(app) {
|
||||
const deletedEmail = user.username
|
||||
const deletedDisplayName = user.displayName || user.username
|
||||
|
||||
for (const [token, session] of state.studySessions) {
|
||||
if (session.userId === user.id) state.studySessions.delete(token)
|
||||
}
|
||||
invalidateAllStudySessions(user.id)
|
||||
|
||||
state.studyUsers = state.studyUsers.filter(u => u.id !== user.id)
|
||||
queueStudyUsersWrite()
|
||||
|
||||
Reference in new issue
Block a user