Stop showing email-derived names in student discussions; v2.2.1
Publish Container / build (push) Successful in 14m56s

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 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5 committed 2026-09-24 11:15:54 -04:00
1 parent 3d3c7175a1
commit 8496e227b3
4 files changed
+32 -12

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "siteforge", "name": "siteforge",
"private": true, "private": true,
"version": "2.2.0", "version": "2.2.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+11 -8
View File
@@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { requireAdminAuth } from '../auth.js' 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 { state } from '../state.js'
import { queueStudyCommentsWrite } from '../data.js' import { queueStudyCommentsWrite } from '../data.js'
import { MAX_STUDY_COMMENTS, MAX_STUDY_COMMENT_LENGTH } from '../config.js' import { MAX_STUDY_COMMENTS, MAX_STUDY_COMMENT_LENGTH } from '../config.js'
@@ -17,12 +17,15 @@ export function register(app) {
const comments = state.studyComments const comments = state.studyComments
.filter(c => c.studySlug === studySlug && c.sectionId === sectionId && c.isApproved === true) .filter(c => c.studySlug === studySlug && c.sectionId === sectionId && c.isApproved === true)
.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
.map(c => ({ .map(c => {
id: c.id, const author = findStudyUserById(c.userId)
displayName: c.displayName, return {
text: c.text, id: c.id,
createdAt: c.createdAt, displayName: author ? getCommunityDisplayName(author) : c.displayName,
})) text: c.text,
createdAt: c.createdAt,
}
})
res.json({ comments }) res.json({ comments })
}) })
@@ -53,7 +56,7 @@ export function register(app) {
studySlug, studySlug,
sectionId, sectionId,
userId: user.id, userId: user.id,
displayName: user.displayName || user.username, displayName: getCommunityDisplayName(user),
text, text,
createdAt: now, createdAt: now,
isApproved: true, isApproved: true,
+9 -3
View File
@@ -9,6 +9,7 @@ import {
findStudyUserById, findStudyUserById,
getStudyAvatarUrl, getStudyAvatarUrl,
getStudyCatalog, getStudyCatalog,
getCommunityDisplayName,
} from '../study-helpers.js' } from '../study-helpers.js'
import { state } from '../state.js' import { state } from '../state.js'
import { import {
@@ -366,13 +367,18 @@ export function register(app) {
const likedBy = Array.isArray(post.likedBy) ? post.likedBy : [] const likedBy = Array.isArray(post.likedBy) ? post.likedBy : []
return { return {
...post, ...post,
authorName: author ? getCommunityDisplayName(author) : post.authorName,
authorAvatarUrl: getStudyAvatarUrl(author || post.authorName || ''), authorAvatarUrl: getStudyAvatarUrl(author || post.authorName || ''),
likedByMe: likedBy.includes(user.id), likedByMe: likedBy.includes(user.id),
likeCount: likedBy.length, likeCount: likedBy.length,
replies: Array.isArray(post.replies) replies: Array.isArray(post.replies)
? post.replies.map(reply => { ? post.replies.map(reply => {
const replyAuthor = findStudyUserById(reply.authorUserId) 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 now = new Date().toISOString()
const authorName = user.displayName?.trim() || (user.username?.includes('@') ? user.username.split('@')[0] : user.username) const authorName = getCommunityDisplayName(user)
const post = { const post = {
id: randomUUID(), id: randomUUID(),
studySlug, studySlug,
@@ -442,7 +448,7 @@ export function register(app) {
const reply = { const reply = {
id: randomUUID(), id: randomUUID(),
authorUserId: user.id, authorUserId: user.id,
authorName: user.displayName?.trim() || (user.username?.includes('@') ? user.username.split('@')[0] : user.username), authorName: getCommunityDisplayName(user),
authorAvatarUrl: getStudyAvatarUrl(user.username), authorAvatarUrl: getStudyAvatarUrl(user.username),
message, message,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
+11
View File
@@ -128,6 +128,17 @@ export function findStudyUserByUsername(username) {
return state.studyUsers.find(user => user.username === normalizeStudyUsername(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 ───────────────────────────────────────────────────────────────── // ── Avatar ─────────────────────────────────────────────────────────────────
export function getStudyAvatarUrl(subject) { export function getStudyAvatarUrl(subject) {