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
@@ -34,6 +34,7 @@ import {
|
||||
CALENDAR_EVENTS_FILE,
|
||||
AUDIT_LOG_FILE,
|
||||
WEBHOOKS_FILE,
|
||||
STUDY_SESSIONS_FILE,
|
||||
EMPTY_HIT_STATS,
|
||||
EMPTY_VISITOR_STATS,
|
||||
DEFAULT_REPLY_TEMPLATES,
|
||||
@@ -496,6 +497,33 @@ export function loadWebhooksFromDisk() {
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// ── Study sessions ──────────────────────────────────────────────────────
|
||||
|
||||
export function queueStudySessionsWrite() {
|
||||
state.studySessionsWritePromise = state.studySessionsWritePromise
|
||||
.then(async () => {
|
||||
await mkdir(DATA_DIR, { recursive: true })
|
||||
const sessions = Array.from(state.studySessions.entries()).map(([token, session]) => ({ token, ...session }))
|
||||
await writeFile(STUDY_SESSIONS_FILE, JSON.stringify({ sessions }, null, 2), 'utf8')
|
||||
})
|
||||
.catch(err => { console.error('[study-sessions] write failed:', err) })
|
||||
}
|
||||
|
||||
export function loadStudySessionsFromDisk() {
|
||||
return readFile(STUDY_SESSIONS_FILE, 'utf8')
|
||||
.then(raw => {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed?.sessions)) return
|
||||
const now = Date.now()
|
||||
for (const entry of parsed.sessions) {
|
||||
if (!entry?.token || !entry?.userId || typeof entry.expiresAt !== 'number') continue
|
||||
if (entry.expiresAt <= now) continue
|
||||
state.studySessions.set(entry.token, { userId: entry.userId, expiresAt: entry.expiresAt })
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
export async function fireWebhooks(eventType, payload) {
|
||||
const targets = state.webhooks.filter(w =>
|
||||
Array.isArray(w.events) && (w.events.includes(eventType) || w.events.includes('*'))
|
||||
@@ -1435,6 +1463,18 @@ export function sanitizeStudyUsers(value) {
|
||||
requestedAt: typeof item.pendingEmailChange.requestedAt === 'string' ? item.pendingEmailChange.requestedAt : null,
|
||||
}
|
||||
: null
|
||||
const pendingEmailVerification = item?.pendingEmailVerification && typeof item.pendingEmailVerification === 'object' && !Array.isArray(item.pendingEmailVerification)
|
||||
? {
|
||||
tokenHash: typeof item.pendingEmailVerification.tokenHash === 'string' ? item.pendingEmailVerification.tokenHash.trim() : '',
|
||||
expiresAt: typeof item.pendingEmailVerification.expiresAt === 'number' ? item.pendingEmailVerification.expiresAt : 0,
|
||||
}
|
||||
: null
|
||||
const pendingPasswordReset = item?.pendingPasswordReset && typeof item.pendingPasswordReset === 'object' && !Array.isArray(item.pendingPasswordReset)
|
||||
? {
|
||||
tokenHash: typeof item.pendingPasswordReset.tokenHash === 'string' ? item.pendingPasswordReset.tokenHash.trim() : '',
|
||||
expiresAt: typeof item.pendingPasswordReset.expiresAt === 'number' ? item.pendingPasswordReset.expiresAt : 0,
|
||||
}
|
||||
: null
|
||||
seen.add(username)
|
||||
out.push({
|
||||
id: typeof item?.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(),
|
||||
@@ -1450,6 +1490,9 @@ export function sanitizeStudyUsers(value) {
|
||||
lastLoginAt: typeof item?.lastLoginAt === 'string' ? item.lastLoginAt : null,
|
||||
reengagementSentAt: typeof item?.reengagementSentAt === 'object' && item.reengagementSentAt !== null && !Array.isArray(item.reengagementSentAt) ? item.reengagementSentAt : {},
|
||||
pendingEmailChange,
|
||||
emailVerified: item?.emailVerified === true,
|
||||
pendingEmailVerification,
|
||||
pendingPasswordReset,
|
||||
twoFaMethod: item?.twoFaMethod === 'app' || item?.twoFaMethod === 'email' ? item.twoFaMethod : null,
|
||||
totpSecret: typeof item?.totpSecret === 'string' && item.totpSecret ? item.totpSecret : null,
|
||||
totpVerified: item?.totpVerified === true,
|
||||
|
||||
Reference in new issue
Block a user