New admin panel - People → Study Users
This commit is contained in:
@@ -3908,6 +3908,93 @@ app.delete('/api/study-account', requireStudyAuth, async (req, res) => {
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
// ── Admin: Study User Management ──────────────────────────────────────────
|
||||
|
||||
app.get('/api/admin/study-users', requireAdminAuth, async (req, res) => {
|
||||
const catalog = getStudyCatalog()
|
||||
const users = await Promise.all(studyUsers.map(async user => {
|
||||
let noteCount = 0
|
||||
try {
|
||||
const notesRaw = await readFile(getUserNotesFilePath(user.id), 'utf8').catch(() => '{}')
|
||||
const notes = JSON.parse(notesRaw)
|
||||
noteCount = Object.values(notes).filter(n => typeof n === 'string' && n.trim()).length
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const enrolledStudies = (user.enrolledStudySlugs ?? []).map(slug => {
|
||||
const study = catalog.find(s => s.slug === slug)
|
||||
return study ? { slug, title: study.title } : { slug, title: slug }
|
||||
})
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName ?? '',
|
||||
createdAt: user.createdAt ?? null,
|
||||
lastLoginAt: user.lastLoginAt ?? null,
|
||||
enrolledStudies,
|
||||
noteCount,
|
||||
subscribeNewsletter: user.subscribeNewsletter !== false,
|
||||
studyRemindersEnabled: user.studyRemindersEnabled === true,
|
||||
}
|
||||
}))
|
||||
|
||||
res.json({ users })
|
||||
})
|
||||
|
||||
app.patch('/api/admin/study-users/:id', requireAdminAuth, (req, res) => {
|
||||
const user = studyUsers.find(u => u.id === req.params.id)
|
||||
if (!user) { res.status(404).json({ message: 'User not found.' }); return }
|
||||
|
||||
const { displayName, newPassword, addEnrollment, removeEnrollment } = req.body ?? {}
|
||||
|
||||
if (typeof displayName === 'string') {
|
||||
user.displayName = displayName.trim().slice(0, 80)
|
||||
}
|
||||
|
||||
if (typeof newPassword === 'string') {
|
||||
if (newPassword.length < 8 || newPassword.length > 200) {
|
||||
res.status(400).json({ message: 'Password must be 8–200 characters.' }); return
|
||||
}
|
||||
user.passwordHash = hashStudyPassword(newPassword)
|
||||
// Revoke all active sessions so they must log in with new password
|
||||
for (const [token, session] of studySessions) {
|
||||
if (session.userId === user.id) studySessions.delete(token)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof addEnrollment === 'string' && addEnrollment.trim()) {
|
||||
const slug = addEnrollment.trim()
|
||||
if (!Array.isArray(user.enrolledStudySlugs)) user.enrolledStudySlugs = []
|
||||
if (!user.enrolledStudySlugs.includes(slug)) user.enrolledStudySlugs.push(slug)
|
||||
}
|
||||
|
||||
if (typeof removeEnrollment === 'string' && removeEnrollment.trim()) {
|
||||
const slug = removeEnrollment.trim()
|
||||
user.enrolledStudySlugs = (user.enrolledStudySlugs ?? []).filter(s => s !== slug)
|
||||
}
|
||||
|
||||
user.updatedAt = new Date().toISOString()
|
||||
queueStudyUsersWrite()
|
||||
res.json({ ok: true, displayName: user.displayName, enrolledStudySlugs: user.enrolledStudySlugs })
|
||||
})
|
||||
|
||||
app.delete('/api/admin/study-users/:id', requireAdminAuth, async (req, res) => {
|
||||
const user = studyUsers.find(u => u.id === req.params.id)
|
||||
if (!user) { res.status(404).json({ message: 'User not found.' }); return }
|
||||
|
||||
for (const [token, session] of studySessions) {
|
||||
if (session.userId === user.id) studySessions.delete(token)
|
||||
}
|
||||
|
||||
studyUsers = studyUsers.filter(u => u.id !== user.id)
|
||||
queueStudyUsersWrite()
|
||||
|
||||
studyNotesCache.delete(user.id)
|
||||
try { await unlink(getUserNotesFilePath(user.id)) } catch { /* no notes file is fine */ }
|
||||
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
app.get('/api/admin-auth/status', async (req, res) => {
|
||||
res.json({
|
||||
authenticated: isValidAdminSession(req),
|
||||
|
||||
Reference in New Issue
Block a user