Work / Learning Platform / Wiki / Synthesis
feature-completeness
Synthesisdraftverified 2026-05-01
SYNTHESIS.FEATURE-COMPLETENESSSummary
A per-feature audit of the codebase, layer by layer: Prisma, GraphQL, NestJS, frontend. Dated 2026-05-01.
Built from
Body
Read the Frontend column with care. This audit is from 2026-05-01. The Settings page was built after it. Checked against the code on 2026-10-04, these "No UI" entries are no longer true:
| The audit says no UI for | Now at |
|---|---|
| disconnect OAuth, my OAuth accounts | security |
| my sessions, revoke session | security |
| change password | security |
| resend verification | profile, and the error state of verify-email |
| update profile | profile |
| add or remove a course instructor, update an enrolment's role | members |
| sync status on the instructor dashboard | dashboard |
Still true on 2026-10-04: no UI to grade or delete a submission (submissions), none for an admin to list or verify users, and none for a student to leave a course. Nothing else in the audit was re-checked, which is why the page is a draft. "Roadmap 5.x" below means FEAT-0005.
Review of the codebase against product requirements (README). Columns: Feature name, Prisma, GQL, NestJS, Frontend. Legend: ✅ Complete | ⚠️ Partial / needs verification | ❌ Missing / gap | 🧪 Verified end-to-end with a smoke test in this validation pass
Question-sync investigation (2026-05-01)
Triggered by "questions are not syncing correctly" against https://github.com/ritesh-firodiya/learning-platform-template. Inspected the cloned content vs. the synced DB rows + the parser/sync code.
What's actually wrong (and what isn't):
| Finding | Severity | Status |
|---|---|---|
Parser harvests <!-- question:db:... --> markers from inside Markdown code spans/fences — introduction/instructor.mdx contains the line Question marker example: `<!-- question:db:Q_INTRO_001 -->` purely as documentation, but the regex pulls it out and phantom-attaches Q_INTRO_001 to the INSTRUCTOR_NOTES phase. |
Major | ✅ Fixed (stripCodeBlocks preserves length, blanks both fenced and inline code before regex matching). Verified: instructor.mdx now extracts 0 markers, all other phase files extract their real markers unchanged. |
Orphan questions never deleted on re-sync — sync deletes questions referenced in MDX but absent from questions.json; if both the JSON entry and the MDX marker are removed in the same edit, the DB row stays forever. |
Minor | ✅ Fixed — sync now collects the union of questions.json IDs + MDX-marker IDs and soft-deletes any DB question not in that union (skipping anything with submissions, for audit). |
Question.lessonId was missing from the GraphQL schema even though it's a NOT NULL column on the Prisma model and the QuestionSubmission GraphQL type already exposed it. |
Minor | ✅ Fixed — added to 0-question.schema.gql and the mapper. |
Question.questionId is @unique globally in the Prisma schema. If you fork the template into a second course, syncing the second copy will hit the unique constraint on Q_INTRO_001..004 and silently fail those question rows (logged per-question, sync continues). |
Major | ⚠️ Not fixed — schema change. Should be @@unique([lessonId, questionId]) only (already declared). Removing the field-level @unique requires a migration. Documented as roadmap follow-up. |
Question.lessonPhases many-to-many relation is declared in Prisma (_LessonPhaseToQuestion join table) but never populated. Phase membership is recomputed from MDX on every lessonPhase.questions request. |
Minor / dead schema | ⚠️ Not fixed — either remove the relation from the schema, or populate during sync (bonus: would let us cache results and skip per-request MDX parsing). Documented as roadmap follow-up. |
Verified working (no bug): all 9 questions from the template (4 in introduction, 5 in sample-onboarding) sync correctly with the right type, options, correctAnswer, order, etc. lessonPhase.questions returns the expected per-phase grouping for both lessons. Phase visibility filters INSTRUCTOR_NOTES from STUDENTs. Submission flow tested earlier still passes.
Curriculum smoke test (2026-05-01)
Verdict: every product flow that doesn't depend on UI surface is working end-to-end. 26/26 GraphQL-level checks pass against a synced learning-platform-template course at /tmp/lms-repos/....
Verified flows (script at /tmp/feature-smoke.sh):
- Public catalog query returns 2 published courses, both
syncStatus=SUCCESS. courseBySlugreturns the course with its 2 lessons +localRepoPath+contentPath.lessonPhases(lessonId)returns 3 phases for STUDENT (PRE_CLASS,LIVE_CLASS,POST_CLASS);INSTRUCTOR_NOTEScorrectly hidden.slides(lessonId)returns 3 slides withcontentPathpointing into the cloned repo.Slide.contentresolves to the actual MDX from disk.LessonPhase.contentresolves to the actual MDX from disk.getAsset(courseId, "course.json")returns the JSON file withmimeType=application/json+encoding=utf-8.getAssetrejects../../../etc/passwdpath traversal (returns null instead of leaking content).- Register a fresh STUDENT, get JWT.
enrollInCoursesucceeds, returnsstatus=ACTIVE.myEnrollmentsshows the new enrollment.- STUDENT-scoped phase query still hides
INSTRUCTOR_NOTESafter auth. - Phase has 1 question (CHECKLIST type), question metadata resolves.
submitQuestionwith the requiredquestionId+courseId+answertriple persists a submission.myQuestionSubmissionsshows the submission to the student.unenrollFromCoursesucceeds.
Caveats found while verifying:
- Express
rate-limitis set to 100 req / 15 min globally (apps/server/src/bootstrap.ts). That's enough to rate-limit a few back-to-back smoke runs from the same IP. For production, either bumpRATE_LIMIT_MAXenv or add a per-IP exemption strategy — otherwise a power-user student walking through a course quickly will get 429s. Severity: minor for normal use, major for testing/onboarding. - GitService
readLocalFileworks because/tmp/lms-repos/...is populated locally (the user already ran the sync). On a Vercel cold start, this directory is empty and content reads will returnnulluntil a freshsyncCourseruns. Documented in.context/ops/runbooks/deployment.md; real fix is the Octokit content-read rewrite (Roadmap 5.10). getAssetfor missing/traversal paths returns{"data":{"getAsset":null}}with no error code. UI must handle null gracefully.
Status as of 2026-05-01 — auth stabilization pass
This round verified auth end-to-end against a running stack and fixed three latent bugs. Every change below is reflected in the per-section tables.
Bugs fixed:
- Refresh-token rotation was a no-op.
refreshTokenre-bcrypt-hashed the input and looked upSession.hashedTokenby equality. bcrypt produces a different output every call, so the lookup never matched — refresh always failed. Even after fixing that, bcrypt's 72-byte input cap caused all refresh tokens for a given user to collide on their shared header+payload prefix, so any of the user's old refresh tokens would match any active session. Fixed by storingsha256(refreshToken)(the JWT signature already makes the token unguessable) and looking up by direct equality. Also added ajticlaim to refresh tokens so two rotations within the same second produce distinct tokens. Verified: rotation now produces a new token; re-using a revoked token is rejected withUNAUTHORIZED. - Email verification + password reset tokens were double-hashed.
AuthorizationServicebcrypt-hashed once, thenVerificationTokenService.createbcrypt-hashed again, storingbcrypt(bcrypt(plaintext)). The lookup didcompare(plaintext, stored)which always returned false. Fixed by removing the second hash. Verified:verifyEmailsucceeds with the matching plaintext and flipsUser.emailVerifiedto true. adminVerifyUserresolver had no schema entry. The resolver was registered but the mutation wasn't declared, so Nest crashed at boot. Added to0-user.schema.gqlwith a matching.mutation.gqldocument.
Hardening added:
- Password strength validator (
shared/utils/password.utils.ts) wired intoregisterandresetPassword. Rejects passwords under 8 chars or without both a letter and a digit. - Refresh tokens now carry a random
jticlaim to make rotation collision-free.
Gaps still open — see "Open security follow-ups" near the bottom.
1. Auth
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Login (email/password) | ✅ User, Session, Account | ✅ login mutation |
🧪 AuthorizationService | ✅ /auth/login, useAuth.login, LoginForm |
| Register | ✅ User, Session | ✅ register mutation |
🧪 AuthorizationService (now enforces password strength) | ✅ /auth/register, useAuth.register, RegisterForm |
| Logout | ✅ Session (deleted by user) | ✅ logout mutation |
✅ AuthorizationResolver (AuthGuard) | ✅ useAuth.logout |
| Refresh token | 🧪 Session.hashedToken now stores sha256(jwt) |
✅ refreshToken mutation |
🧪 AuthorizationService — fixed bcrypt-truncation bug, added jti for collision-free rotation |
✅ Apollo error link auto-refreshes on UNAUTHENTICATED, queues concurrent requests |
| OAuth login (Google, GitHub) | ✅ Account, User | ✅ loginWithOAuth mutation |
⚠️ AuthorizationService — works, but state parameter is accepted and never validated server-side (CSRF gap, see follow-ups) |
✅ Popup flow, useAuth.authenticateWithOAuth |
| Connect OAuth | ✅ Account | ✅ connectOAuth mutation |
⚠️ Same state gap as login |
✅ useAuth.authenticateWithOAuth(mode='connect') |
| Disconnect OAuth | ✅ Account (soft-delete) | ✅ disconnectOAuth mutation |
✅ AuthorizationService | ❌ No UI |
| Request password reset | ✅ VerificationToken (PASSWORD_RESET) | ✅ requestPasswordReset mutation |
✅ AuthorizationService | ✅ /auth/forgot-password, ForgotPasswordForm |
| Reset password (with token) | 🧪 VerificationToken (single-bcrypt now) | ✅ resetPassword mutation, validatePasswordResetToken query |
🧪 AuthorizationService — double-hash fixed; password-strength enforced on the new password | ✅ /auth/reset-password, ResetPasswordForm |
| Verify email | 🧪 VerificationToken (single-bcrypt now) | ✅ verifyEmail mutation |
🧪 AuthorizationService — verified flips User.emailVerified end-to-end |
✅ /auth/verify-email, VerifyEmailForm |
| Resend verification email | ✅ VerificationToken (3/hour rate limit) | ✅ resendVerification mutation |
✅ AuthorizationService | ❌ No UI |
| Admin verify user | ✅ User.emailVerified | ✅ adminVerifyUser mutation (added in this pass) |
✅ UserResolver | ❌ No UI |
Auth summary: Email/password auth, JWT rotation, password reset, email verification, and OAuth state CSRF validation are now verified end-to-end. Disconnect OAuth, resend verification, admin verify user, and admin user list have no UI yet.
Session persistence — verified
| Concern | Status | Notes |
|---|---|---|
| Token storage | ✅ Works | localStorage via Zustand persist + mirrored to accessToken cookie (path=/, max-age 7d, SameSite=Lax) so Next middleware/SSR can see auth |
| Reload survival | 🧪 Verified | AuthProvider rehydrates from cookie if store is empty, then runs me to validate |
| Auto-refresh on 401 | ✅ Works | Apollo errorLink queues concurrent requests during refresh; refresh now actually succeeds |
| Rotation security | 🧪 Verified | New refresh token has unique jti; re-use of an old token is rejected |
| Multi-device | ✅ Allowed | Each login/register creates a new Session row; logout revokes all of the user's sessions (deleteByUserId) |
Open security follow-ups (prioritised)
Resolved in the production-readiness pass:
- ✅ A — OAuth state CSRF —
useAuthnow stores generated state insessionStorage, the callback page forwardsstateviapostMessage, anduseAuthrejects mismatch before callingloginWithOAuth/connectOAuth. - ✅ C — Email-verification gate —
EmailVerifiedGuardis wired ontoenrollInCourseandsubmitQuestion. Disabled by default; flip viaREQUIRE_EMAIL_VERIFICATION=trueonce SES/email is verified working. - ✅ D — Cross-tab logout —
AuthProviderlistens forstorageevents on theauth-storagekey and clears local auth state when a sibling tab logs out. - ✅ E — Default JWT secret —
env.validator.tsrejects placeholder secrets, malformedENCRYPTION_KEY, length under 32, andJWT_SECRET===JWT_REFRESH_SECRET. Refuses to boot inNODE_ENV=production.
Still open:
| # | Item | Severity | Complexity |
|---|---|---|---|
| B | Access token in localStorage + non-httpOnly cookie. XSS can steal both. Move to httpOnly + Secure cookie set by the server; have Apollo read it server-side or via a signed token endpoint. Substantive refactor — defer to a hardening phase. | Major | High |
| F | Password strength. Currently 8 chars + letter + digit. Consider zxcvbn or HIBP check before opening signup to the public. | Minor | Low |
| G | Vercel sync limitation. GitService content reads expect a populated /tmp clone; Vercel cold starts arrive with an empty /tmp. Mitigations + the Octokit follow-up are documented in .context/ops/runbooks/deployment.md. (Roadmap 5.10) |
Major | Medium |
2. User & Profile
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Current user (me) | ✅ User | ✅ me query |
✅ UserResolver (AuthGuard) | ✅ AuthProvider useMeQuery, useAuthStore |
| User profile (public) | ✅ User | ✅ userProfile(id) query |
✅ UserResolver | ⚠️ Query exists; profile page not verified |
| Update profile | ✅ User | ✅ updateUser mutation |
✅ UserResolver (AuthGuard) | ⚠️ Not verified (settings/profile page) |
| Change password | ✅ User.password | ✅ changePassword mutation |
✅ UserResolver, UserService | ⚠️ GQL + backend present; change-password UI not verified |
| Admin: list users | ✅ User | ✅ users query (ADMIN) |
✅ UserResolver (RolesGuard) | ⚠️ Admin UI not verified |
| Admin: delete user | ✅ User | ✅ deleteUser mutation |
✅ UserResolver (ADMIN) | ⚠️ Admin UI not verified |
| Check email availability | ✅ User | ✅ isEmailAvailable query |
✅ UserResolver | ⚠️ Can be used by RegisterForm |
3. Sessions & Accounts
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| My sessions | ✅ Session | ✅ mySessions query |
✅ SessionResolver (AuthGuard) | ⚠️ GQL + backend present; sessions UI not verified |
| Revoke session | ✅ Session.isRevoked | ✅ revokeSession mutation |
✅ SessionResolver | ⚠️ Sessions management UI not verified |
| My OAuth accounts | ✅ Account | ✅ myAccounts query |
✅ AccountResolver (AuthGuard) | ⚠️ Used in connect OAuth flow; accounts list UI not verified |
4. Instructor Onboarding
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Role assignment | ✅ User.role (INSTRUCTOR) | ✅ updateUser (role) — typically ADMIN |
✅ UserResolver (admin) | ⚠️ No dedicated “instructor onboarding” flow in README; role set by admin or seed |
| First-time instructor flow | — | — | — | ⚠️ My Courses shows instructor view by role; no separate onboarding wizard |
Note: README does not define a dedicated “Instructor Onboarding” flow; instructors are users with role INSTRUCTOR and use “My Courses” and course link/sync.
5. Student Onboarding
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Enrollment | ✅ Enrollment | ✅ enrollInCourse mutation |
🧪 EnrollmentService | ✅ Course page / course card: useEnrollInCourseMutation |
| My enrollments | ✅ Enrollment | ✅ myEnrollments query |
🧪 EnrollmentResolver | ✅ My Courses (student): useMyEnrollmentsQuery |
| Unenroll | ✅ Enrollment | ✅ unenrollFromCourse mutation |
🧪 EnrollmentService | ⚠️ UI not verified |
| First-time student flow | — | — | — | ⚠️ Browse courses → enroll; no dedicated onboarding wizard |
6. Course (Curriculum)
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Course list (catalog) | ✅ Course | ✅ courses query (where, orderBy, pagination) |
🧪 CourseService | ✅ /courses, home: useCoursesQuery, CourseFilterStore |
| Course by slug | ✅ Course | ✅ courseBySlug query |
🧪 CourseResolver | ✅ Course page, instructor dashboard: useCourseBySlugQuery |
| Course by id | ✅ Course | ✅ course query |
✅ CourseResolver | ✅ Used by other resolvers / optional client use |
| Link course (connect repo) | ✅ Course, CourseInstructor | ✅ linkCourse mutation (LinkCourseInput) |
🧪 CurriculumService (GitService clone) — verified by the synced course in DB at /tmp/lms-repos/... |
✅ Link course modal: useLinkCourseMutation |
| Unlink course | ✅ Course | ✅ unlinkCourse mutation |
✅ CurriculumResolver | ✅ CourseInstructorControls: useUnlinkCourseMutation |
| Sync course (manual) | ✅ Course, Lesson, LessonPhase, Slide, Question | ✅ syncCourse mutation |
🧪 CurriculumService — syncStatus=SUCCESS produced 2 lessons / 8 phases / 6 slides / 9 questions from the linked template repo |
✅ CourseInstructorControls: useSyncCourseMutation |
| Update course (e.g. publish) | ✅ Course | ✅ updateCourse mutation |
✅ CurriculumResolver | ✅ CourseInstructorControls: useUpdateCourseMutation (toggle isPublished) |
| My courses (instructor) | ✅ CourseInstructor, Course | ✅ myCourses query |
✅ CurriculumResolver (AuthGuard, instructor filter) | ✅ My Courses (instructor): useMyCoursesQuery |
| Get asset (file from repo) | — (GitService) | ✅ getAsset(courseId, assetPath) query |
🧪 CourseService — course.json returns json mimeType utf-8; path traversal returns null |
✅ Used for content (e.g. MDX/images) in course view |
| Add course (create from repo) | — | — | — | ✅ Implemented as "Link course" (no separate "create" without repo) |
7. Course Instructors
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Add instructor to course | ✅ CourseInstructor | ✅ addCourseInstructor mutation |
✅ CourseInstructorResolver (ADMIN/INSTRUCTOR) | ⚠️ GQL + backend present; add-instructor UI not verified |
| Remove instructor | ✅ CourseInstructor | ✅ removeCourseInstructor mutation |
✅ CourseInstructorResolver | ⚠️ UI not verified |
8. Lesson & Phases & Slides
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Lessons list (by course) | ✅ Lesson | ✅ Field Course.lessons + lessons query |
🧪 LessonResolver — 2 lessons resolved on test course | ✅ Course page, sidebar |
| Lesson by id | ✅ Lesson | ✅ lesson query |
✅ LessonResolver | ✅ useLessonQuery (course content) |
| Lesson by slug | ✅ Lesson | ✅ lessonBySlug(courseId, slug) query |
✅ LessonResolver | ✅ Course content navigation |
| Lesson phases (Pre/Live/Post/Instructor) | ✅ LessonPhase | ✅ lessonPhase, lessonPhases queries, Lesson.phases |
🧪 LessonPhaseResolver — @auth directive enforced; service-layer filter hides INSTRUCTOR_NOTES from STUDENTs (verified) |
✅ lesson-phase-content, phase navigation |
| Slides (per lesson) | ✅ Slide | ✅ slide, slides queries, Lesson.slides |
🧪 SlideResolver — 3 slides resolved on test lesson | ✅ slide-content, phase navigation |
| Live content (MDX from repo) | ✅ LessonPhase.contentPath, Slide.contentPath | ✅ LessonPhase.content + Slide.content resolved at request time |
🧪 GitService.readLocalFile reads MDX from cloned repo on disk — both phase content and slide content verified end-to-end against the cloned learning-platform-template |
✅ Markdown renderer, GetAsset |
9. Question (Definition)
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Question by id | ✅ Question | ✅ question query |
✅ QuestionResolver | ✅ Used in question renderer, submissions |
| Question by questionId | ✅ Question | ✅ questionByQuestionId query |
✅ QuestionResolver | ✅ Used for content-driven questions |
| Questions list (filter) | ✅ Question | ✅ questions query, LessonPhase.questions |
🧪 QuestionResolver — 9 questions seeded from repo, resolves on phase + course | ✅ Instructor questions page: useQuestionsQuery |
| Question types (Checklist, Poll, MCQ, Fill-in-blank, Code script) | ✅ QuestionType enum | ✅ In Question type and inputs | 🧪 Question entity — CHECKLIST verified end-to-end with [true,false] answer |
✅ question-renderer, type-specific components |
10. Question Submission (Student & Instructor)
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Submit answer | ✅ QuestionSubmission | ✅ submitQuestion mutation (requires questionId + courseId + answer: JSON) |
🧪 QuestionSubmissionService — CHECKLIST submission persisted with answer JSON | ✅ question-renderer: useSubmitQuestionMutation |
| My submissions | ✅ QuestionSubmission | ✅ myQuestionSubmissions query |
🧪 QuestionSubmissionResolver — student sees own submission immediately after submit | ✅ useMyQuestionSubmissionsQuery (existing-submission display) |
| List submissions (instructor) | ✅ QuestionSubmission | ✅ questionSubmissions query |
✅ QuestionSubmissionResolver | ✅ Instructor submissions page: useQuestionSubmissionsQuery, GraphQLDataTable |
| Update submission (grade) | ✅ QuestionSubmission (isCorrect, points, gradedAt) | ✅ updateQuestionSubmission mutation |
✅ QuestionSubmissionResolver (INSTRUCTOR/ADMIN) | ❌ No grade/edit UI — table is read-only; mutation exists but not used |
| Delete submission (instructor) | ✅ QuestionSubmission | ✅ deleteQuestionSubmission mutation |
✅ QuestionSubmissionResolver | ⚠️ No UI found |
| Submission stats | — | ✅ questionSubmissionStats query |
✅ QuestionSubmissionResolver (INSTRUCTOR/ADMIN) | ⚠️ Can be used for dashboard; not verified |
| Instructor view correct answer | ✅ Question.correctAnswer, PhaseVisibility | ✅ Question type (correctAnswer), phase visibility | ✅ Resolvers return by role | ✅ view-answer-button, question-answer-display (instructor) |
11. Enrollment & Progress
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Enroll in course | ✅ Enrollment | ✅ enrollInCourse mutation |
✅ EnrollmentResolver | ✅ useEnrollInCourseMutation (course header, course card) |
| Unenroll | ✅ Enrollment | ✅ unenrollFromCourse mutation |
✅ EnrollmentResolver | ⚠️ GQL + backend present; unenroll UI not verified |
| Update enrollment (e.g. status) | ✅ Enrollment | ✅ updateEnrollment mutation |
✅ EnrollmentResolver | ⚠️ UI not verified |
| Enrollment stats (instructor) | ✅ Enrollment | ✅ enrollmentStats query |
✅ EnrollmentResolver (INSTRUCTOR/ADMIN) | ⚠️ Can be used in dashboard |
| Lesson progress (Pre/Live/Post completed) | ✅ LessonProgress | ✅ createLessonProgress, updateLessonProgress, lessonProgressList, etc. |
✅ LessonProgressResolver, LessonProgressService | ✅ lesson-progress-indicator: useLessonProgressListQuery, useMyEnrollmentsQuery; progress-overview |
| Create/update progress | ✅ LessonProgress | ✅ create/update/delete mutations | ✅ LessonProgressResolver | ⚠️ Progress may be updated implicitly or via dedicated UI; not fully traced |
12. Analytics & Metrics
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Course stats (enrollments, lessons, submissions) | — | ✅ courseStats(id) query |
✅ AnalyticsResolver, AnalyticsService | ✅ Instructor dashboard: useCourseStatsQuery |
| Enrollment stats | — | ✅ enrollmentStats(courseId) query |
✅ AnalyticsResolver | ⚠️ Backend present; dashboard usage not verified |
| Health / version | — | ✅ _version, _ping (metrics) |
✅ MetricsResolver, health controller | ✅ Used for health checks |
13. Verification Token (Internal)
| Feature | Prisma | GQL | NestJS | Frontend |
|---|---|---|---|---|
| Validate password reset token | ✅ VerificationToken | ✅ validatePasswordResetToken query |
✅ VerificationTokenResolver | ✅ Reset password flow (token validation) |
| Use verification token (consume) | ✅ VerificationToken | ✅ useVerificationToken mutation |
✅ VerificationTokenResolver | ✅ Used in reset/verify flows |
Summary of Gaps
Resolved in this pass (2026-05-01):
Auth + verification round:
— added with mutation document.adminVerifyUsernot in GQL schemaAuth forms (Forgot/Reset/Verify) missing under— restored.packages/ui/src/components/auth/Refresh token never works— fixed (sha256 storage +jtifor rotation).Email verification + password reset always fail (double-hash)— fixed.No password strength validation— added.
Production-readiness round:
OAuth— sessionStorage + popupstateCSRFpostMessageround-trip.Email-verification gate—EmailVerifiedGuardonenrollInCourse+submitQuestion, opt-in via env flag.Cross-tab logout—storagelistener inAuthProvider.Default JWT secret—env.validator.tsrefuses placeholder/short secrets in prod.Wrong-project URL— replaced withhttps://api.dwarseva.comin CIvars.NEXT_PUBLIC_API_URL; stale Cloudflare workflow removed.— migrated to/api/*route deprecation warning{*splat}for path-to-regexp v8.— documented inGITHUB_TOKENundocumented.env.example+DEPLOYMENT.md; warning logged at prod startup if unset.No deployment doc—.context/ops/runbooks/deployment.mdcovers required Vercel env vars, OAuth callback URLs, sync limitation, smoke test.
Still open:
- Vercel
/tmpephemeral filesystem —GitServicecontent reads need an Octokit-fallback rewrite (Roadmap 5.10). - localStorage XSS exposure — access token also lives in a non-httpOnly cookie. Roadmap follow-up B.
- No UI for: disconnect OAuth, resend verification, admin verify user, admin user list, my sessions / revoke session, my OAuth accounts list, change password, unenroll, add/remove course instructor, instructor grade submission (
updateQuestionSubmission), delete submission. - Profile / settings page not verified —
updateUser,userProfilequeries exist; client integration not traced. - Logging + error tracking — Sentry wire-up pending (Roadmap 5.6).
- Sync-job observability surface —
Course.syncStatusetc. tracked but not all surfaced on instructor dashboard (Roadmap 5.7). - DB backup runbook — undocumented (Roadmap 5.8).
- No tests — Phase 4 entirely.
How This Was Done
- Prisma:
packages/db/prisma/schema.prisma— models and enums. - GQL:
packages/gql/schema.gql(merged) andpackages/gql/src/**/*.gqlfor operations;packages/gql/src/**/0-*.schema.gqlfor schema fragments. - NestJS:
apps/server/src/api/**/*.resolver.ts,*.service.ts, and related modules. - Frontend:
apps/website(Next.js) and sharedpackages/ui— pages underapp/, components usinggql/clienthooks andui/components.
Last refresh
2026-05-01 — the audit's own date. The table above was added on 2026-10-04; the audit itself was not re-run. tasks.md T006.
Sources
- (synthesis pages cite wiki pages, not raw files)