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) {