Completed studies: finishing a study keeps access after leaving; certificates always shown; no reminders for finished studies; v2.11.0
Publish Container / build (push) Successful in 15m31s
Publish Container / build (push) Successful in 15m31s
- Completion is stored on the user (completedStudySlugs, sticky) when every lesson is done; backfilled at startup - Lessons, notes, progress, quiz, community and certificate stay open for completed studies after leaving the enrollment - Dashboard/My Account list completed studies with Review Lessons and certificate access - Study page shows a completed banner - Lesson-release reminders, weekly digest and re-engagement emails skip finished studies - Fix: streak fields (current/longest/last studied) and firstCompletionAt were dropped on every restart when users were loaded - Tests for completion/access/re-engagement Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7301982bb1
commit
bb83f3e8c5
13 files changed
+221
-52
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
import { requireAdminAuth } from '../auth.js'
|
||||
import { requireStudyAuth, normalizeStudySlug, isStudyUserEnrolled, getStudyTitleBySlug } from '../study-helpers.js'
|
||||
import { requireStudyAuth, normalizeStudySlug, hasStudyAccess, getStudyTitleBySlug, getRequiredSectionIds, markStudyCompletedIfDone } from '../study-helpers.js'
|
||||
import { state } from '../state.js'
|
||||
import { loadUserProgress, queueStudyCertificatesWrite } from '../data.js'
|
||||
import { sendStudyCertificateEmail, getCanonicalBaseUrl } from '../email.js'
|
||||
@@ -15,20 +15,6 @@ const publicCertRateLimiter = rateLimit({
|
||||
message: { message: 'Too many certificate lookups. Please wait a moment.' },
|
||||
})
|
||||
|
||||
/**
|
||||
* Returns the list of required section IDs for a study. Every lesson is required, so a
|
||||
* certificate is only available once the whole study has been released and completed.
|
||||
*/
|
||||
function getRequiredSectionIds(studySlug) {
|
||||
const content = state.cachedSiteContent
|
||||
if (!content || !Array.isArray(content.studies)) return null
|
||||
const study = content.studies.find(s => s?.slug === studySlug)
|
||||
if (!study || !Array.isArray(study.sections)) return null
|
||||
return study.sections
|
||||
.filter(s => s?.id)
|
||||
.map(s => s.id)
|
||||
}
|
||||
|
||||
export function register(app) {
|
||||
// GET — check eligibility and return existing certificate token if one exists
|
||||
app.get('/api/study-certificate/:studySlug', requireStudyAuth, async (req, res) => {
|
||||
@@ -36,7 +22,7 @@ export function register(app) {
|
||||
const studySlug = normalizeStudySlug(req.params.studySlug)
|
||||
if (!studySlug) { res.status(400).json({ message: 'Invalid study slug.' }); return }
|
||||
|
||||
if (!isStudyUserEnrolled(user, studySlug)) {
|
||||
if (!hasStudyAccess(user, studySlug)) {
|
||||
res.status(403).json({ message: 'Not enrolled in this study.' }); return
|
||||
}
|
||||
|
||||
@@ -49,6 +35,8 @@ export function register(app) {
|
||||
const completedIds = progress.byStudy[studySlug]?.completedSectionIds ?? []
|
||||
const eligible = requiredIds.every(id => completedIds.includes(id))
|
||||
|
||||
if (eligible) markStudyCompletedIfDone(user, studySlug, completedIds)
|
||||
|
||||
const existing = state.studyCertificates.find(c => c.userId === user.id && c.studySlug === studySlug)
|
||||
|
||||
res.json({
|
||||
@@ -67,7 +55,7 @@ export function register(app) {
|
||||
const studySlug = normalizeStudySlug(req.params.studySlug)
|
||||
if (!studySlug) { res.status(400).json({ message: 'Invalid study slug.' }); return }
|
||||
|
||||
if (!isStudyUserEnrolled(user, studySlug)) {
|
||||
if (!hasStudyAccess(user, studySlug)) {
|
||||
res.status(403).json({ message: 'Not enrolled in this study.' }); return
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user