From 8496e227b30db133921d04dd95333fba19d195e5 Mon Sep 17 00:00:00 2001 From: nmemmert Date: Thu, 24 Sep 2026 11:15:54 -0400 Subject: [PATCH] Stop showing email-derived names in student discussions; v2.2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Community posts/replies fell back to the local part of the student's email (e.g. "student.test" from student.test@versebyverse.local) whenever they hadn't set a display name. The per-lesson discussion comments were worse — they fell back to the FULL email address, and that endpoint has no auth requirement, so it was publicly readable. Adds getCommunityDisplayName() in study-helpers.js: uses the student's display name when set, otherwise assigns a stable "Student XXXX" handle derived from their account id — never their email. Both the community board and lesson comments now resolve names live from the author's current account at read time (falling back to the stored snapshot only if the account was deleted), so this also fixes any already-posted content without a data migration. Co-Authored-By: Claude Sonnet 5 --- package.json | 2 +- server/routes/study-comments.js | 19 +++++++++++-------- server/routes/study-data.js | 12 +++++++++--- server/study-helpers.js | 11 +++++++++++ 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 2360e62..2f95496 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.2.0", + "version": "2.2.1", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/study-comments.js b/server/routes/study-comments.js index 934e502..d420dce 100644 --- a/server/routes/study-comments.js +++ b/server/routes/study-comments.js @@ -1,6 +1,6 @@ import { randomUUID } from 'node:crypto' import { requireAdminAuth } from '../auth.js' -import { requireStudyAuth, normalizeStudySlug, normalizeLessonSectionId, isStudyUserEnrolled } from '../study-helpers.js' +import { requireStudyAuth, normalizeStudySlug, normalizeLessonSectionId, isStudyUserEnrolled, findStudyUserById, getCommunityDisplayName } from '../study-helpers.js' import { state } from '../state.js' import { queueStudyCommentsWrite } from '../data.js' import { MAX_STUDY_COMMENTS, MAX_STUDY_COMMENT_LENGTH } from '../config.js' @@ -17,12 +17,15 @@ export function register(app) { const comments = state.studyComments .filter(c => c.studySlug === studySlug && c.sectionId === sectionId && c.isApproved === true) .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) - .map(c => ({ - id: c.id, - displayName: c.displayName, - text: c.text, - createdAt: c.createdAt, - })) + .map(c => { + const author = findStudyUserById(c.userId) + return { + id: c.id, + displayName: author ? getCommunityDisplayName(author) : c.displayName, + text: c.text, + createdAt: c.createdAt, + } + }) res.json({ comments }) }) @@ -53,7 +56,7 @@ export function register(app) { studySlug, sectionId, userId: user.id, - displayName: user.displayName || user.username, + displayName: getCommunityDisplayName(user), text, createdAt: now, isApproved: true, diff --git a/server/routes/study-data.js b/server/routes/study-data.js index 47e290e..9d27bcf 100644 --- a/server/routes/study-data.js +++ b/server/routes/study-data.js @@ -9,6 +9,7 @@ import { findStudyUserById, getStudyAvatarUrl, getStudyCatalog, + getCommunityDisplayName, } from '../study-helpers.js' import { state } from '../state.js' import { @@ -366,13 +367,18 @@ export function register(app) { const likedBy = Array.isArray(post.likedBy) ? post.likedBy : [] return { ...post, + authorName: author ? getCommunityDisplayName(author) : post.authorName, authorAvatarUrl: getStudyAvatarUrl(author || post.authorName || ''), likedByMe: likedBy.includes(user.id), likeCount: likedBy.length, replies: Array.isArray(post.replies) ? post.replies.map(reply => { const replyAuthor = findStudyUserById(reply.authorUserId) - return { ...reply, authorAvatarUrl: getStudyAvatarUrl(replyAuthor || reply.authorName || '') } + return { + ...reply, + authorName: replyAuthor ? getCommunityDisplayName(replyAuthor) : reply.authorName, + authorAvatarUrl: getStudyAvatarUrl(replyAuthor || reply.authorName || ''), + } }) : [], } @@ -398,7 +404,7 @@ export function register(app) { } const now = new Date().toISOString() - const authorName = user.displayName?.trim() || (user.username?.includes('@') ? user.username.split('@')[0] : user.username) + const authorName = getCommunityDisplayName(user) const post = { id: randomUUID(), studySlug, @@ -442,7 +448,7 @@ export function register(app) { const reply = { id: randomUUID(), authorUserId: user.id, - authorName: user.displayName?.trim() || (user.username?.includes('@') ? user.username.split('@')[0] : user.username), + authorName: getCommunityDisplayName(user), authorAvatarUrl: getStudyAvatarUrl(user.username), message, createdAt: new Date().toISOString(), diff --git a/server/study-helpers.js b/server/study-helpers.js index b68c9e5..50f03e8 100644 --- a/server/study-helpers.js +++ b/server/study-helpers.js @@ -128,6 +128,17 @@ export function findStudyUserByUsername(username) { return state.studyUsers.find(user => user.username === normalizeStudyUsername(username)) } +// ── Community display name ────────────────────────────────────────────────── + +// Never derived from the student's email — falls back to a stable, anonymous +// handle assigned from their account id so nobody's address leaks into posts. +export function getCommunityDisplayName(user) { + const displayName = typeof user?.displayName === 'string' ? user.displayName.trim() : '' + if (displayName) return displayName + const id = typeof user?.id === 'string' ? user.id : '' + return `Student ${(id.replace(/-/g, '').slice(0, 4) || '0000').toUpperCase()}` +} + // ── Avatar ───────────────────────────────────────────────────────────────── export function getStudyAvatarUrl(subject) {