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>
126 lines
4.8 KiB
JavaScript
126 lines
4.8 KiB
JavaScript
import { randomUUID } from 'node:crypto'
|
|
import { requireAdminAuth } from '../auth.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'
|
|
|
|
export function register(app) {
|
|
// GET public-approved comments for a section (no auth)
|
|
app.get('/api/study-comments/:studySlug/:sectionId', (req, res) => {
|
|
const studySlug = normalizeStudySlug(req.params.studySlug)
|
|
const sectionId = normalizeLessonSectionId(req.params.sectionId)
|
|
if (!studySlug || !sectionId) {
|
|
res.status(400).json({ message: 'Invalid study slug or section id.' }); return
|
|
}
|
|
|
|
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 => {
|
|
const author = findStudyUserById(c.userId)
|
|
return {
|
|
id: c.id,
|
|
displayName: author ? getCommunityDisplayName(author) : c.displayName,
|
|
text: c.text,
|
|
createdAt: c.createdAt,
|
|
}
|
|
})
|
|
|
|
res.json({ comments })
|
|
})
|
|
|
|
// POST — enrolled study users post a comment (auto-approved for enrolled users)
|
|
app.post('/api/study-comments/:studySlug/:sectionId', requireStudyAuth, (req, res) => {
|
|
const user = req.studyUser
|
|
const studySlug = normalizeStudySlug(req.params.studySlug)
|
|
const sectionId = normalizeLessonSectionId(req.params.sectionId)
|
|
|
|
if (!studySlug || !sectionId) {
|
|
res.status(400).json({ message: 'Invalid study slug or section id.' }); return
|
|
}
|
|
|
|
if (!isStudyUserEnrolled(user, studySlug)) {
|
|
res.status(403).json({ message: 'Please enroll in this study to leave a comment.' }); return
|
|
}
|
|
|
|
const text = typeof req.body?.text === 'string' ? req.body.text.trim() : ''
|
|
if (!text || text.length < 2 || text.length > MAX_STUDY_COMMENT_LENGTH) {
|
|
res.status(400).json({ message: `Comment must be between 2 and ${MAX_STUDY_COMMENT_LENGTH} characters.` }); return
|
|
}
|
|
|
|
// Auto-approve for enrolled users
|
|
const now = new Date().toISOString()
|
|
const comment = {
|
|
id: randomUUID(),
|
|
studySlug,
|
|
sectionId,
|
|
userId: user.id,
|
|
displayName: getCommunityDisplayName(user),
|
|
text,
|
|
createdAt: now,
|
|
isApproved: true,
|
|
approvedAt: now,
|
|
}
|
|
|
|
state.studyComments.unshift(comment)
|
|
if (state.studyComments.length > MAX_STUDY_COMMENTS) {
|
|
state.studyComments = state.studyComments.slice(0, MAX_STUDY_COMMENTS)
|
|
}
|
|
queueStudyCommentsWrite()
|
|
|
|
res.status(201).json({
|
|
ok: true,
|
|
comment: {
|
|
id: comment.id,
|
|
displayName: comment.displayName,
|
|
text: comment.text,
|
|
createdAt: comment.createdAt,
|
|
},
|
|
})
|
|
})
|
|
|
|
// DELETE — enrolled users can delete their own comment
|
|
app.delete('/api/study-comments/:commentId', requireStudyAuth, (req, res) => {
|
|
const user = req.studyUser
|
|
const { commentId } = req.params
|
|
const idx = state.studyComments.findIndex(c => c.id === commentId && c.userId === user.id)
|
|
if (idx === -1) {
|
|
res.status(404).json({ message: 'Comment not found.' }); return
|
|
}
|
|
state.studyComments.splice(idx, 1)
|
|
queueStudyCommentsWrite()
|
|
res.json({ ok: true })
|
|
})
|
|
|
|
// Admin: list all comments (optionally filter by studySlug)
|
|
app.get('/api/admin-study-comments', requireAdminAuth, (req, res) => {
|
|
const { studySlug, sectionId, approved } = req.query
|
|
let comments = state.studyComments
|
|
if (studySlug) comments = comments.filter(c => c.studySlug === studySlug)
|
|
if (sectionId) comments = comments.filter(c => c.sectionId === sectionId)
|
|
if (approved === 'true') comments = comments.filter(c => c.isApproved === true)
|
|
if (approved === 'false') comments = comments.filter(c => c.isApproved !== true)
|
|
res.json({ comments: comments.slice(0, 500) })
|
|
})
|
|
|
|
// Admin: approve a comment
|
|
app.post('/api/admin-study-comments/:commentId/approve', requireAdminAuth, (req, res) => {
|
|
const comment = state.studyComments.find(c => c.id === req.params.commentId)
|
|
if (!comment) { res.status(404).json({ message: 'Comment not found.' }); return }
|
|
comment.isApproved = true
|
|
comment.approvedAt = new Date().toISOString()
|
|
queueStudyCommentsWrite()
|
|
res.json({ ok: true, comment })
|
|
})
|
|
|
|
// Admin: delete any comment
|
|
app.delete('/api/admin-study-comments/:commentId', requireAdminAuth, (req, res) => {
|
|
const idx = state.studyComments.findIndex(c => c.id === req.params.commentId)
|
|
if (idx === -1) { res.status(404).json({ message: 'Comment not found.' }); return }
|
|
state.studyComments.splice(idx, 1)
|
|
queueStudyCommentsWrite()
|
|
res.json({ ok: true })
|
|
})
|
|
}
|