login and certification fixed - #4
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR removes comprehensive documentation files, refactors the Convex backend to support certificate eligibility calculations through progress summarization, introduces a centralized Changes
Sequence Diagram(s)sequenceDiagram
participant Frontend
participant API as API Route
participant Convex
participant DB as Database
rect rgb(200, 220, 255)
note over Frontend,DB: New Client Factory Pattern
Frontend->>API: Request
API->>API: createConvexClient()
activate API
API->>Convex: Query/Mutation (with auth)
Convex->>DB: Read/Write
DB-->>Convex: Data
Convex-->>API: Result
deactivate API
API-->>Frontend: Response
end
rect rgb(220, 200, 255)
note over Frontend,Convex: Progress Summarization Flow
Convex->>DB: Fetch progress records by contentItemId
DB-->>Convex: Multiple attempts per item
Convex->>Convex: summarizeProgressByContentItem()<br/>Aggregate: bestScore, latest, attempts, completion
Convex->>Convex: checkEligibility()<br/>Calculate grade vs threshold<br/>Check per-item pass status
Convex-->>Frontend: Certificate eligibility + metrics
end
rect rgb(200, 255, 220)
note over Frontend,DB: Quiz Result Update Flow
Frontend->>API: recordCompletion(quiz, score)
API->>Convex: recordCompletion mutation
Convex->>DB: Create/update progress record<br/>Update bestScore, latest, attempts
Convex->>Convex: recalculateCourseProgress<br/>Aggregate item summaries<br/>Recalc course grade
DB-->>Convex: Updated progress state
Convex-->>API: New course progress + certificate eligibility
API-->>Frontend: Render certificate status & results
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)✅ Unit Test PR creation complete.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
convex/progress.ts (1)
43-47: Guard against division by zero for percentage.maxScore can be 0 via args.maxScore/contentItem.maxPoints. Add validation before computing percentage.
Apply:
- const maxScore = args.maxScore ?? contentItem.maxPoints ?? 100; - const percentage = (args.score / maxScore) * 100; + const maxScore = args.maxScore ?? contentItem.maxPoints ?? 100; + if (maxScore <= 0) { + throw new Error("maxScore must be greater than zero"); + } + const percentage = (args.score / maxScore) * 100;convex/completions.ts (3)
353-369: Authorization gap: query exposes other users’ attempts.getQuizAttemptHistory accepts arbitrary userId with no auth check. Validate identity and ensure args.userId matches the caller, or drop the param and derive userId server‑side.
Apply:
handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) throw new Error("Not authenticated"); + const caller = await ctx.db.query("users").withIndex("by_email", q => q.eq("email", identity.email!)).first(); + if (!caller || caller._id !== args.userId) throw new Error("Forbidden");
374-389: Authorization gap: progress read can leak data cross‑user.Same concern as above for getContentItemProgress. Gate by identity or remove userId from args.
Apply the same validation snippet here.
395-441: Authorization gap: course progress read can leak data cross‑user.calculateCourseProgress also trusts userId; add the same identity check before proceeding.
src/components/learnspace/quizzes-panel.tsx (1)
49-62: PII and sensitive data in client logs; gate or remove in production.Multiple console logs leak user/session, quiz answers, and scores. This is risky and noisy.
Apply a simple debug guard:
- useEffect(() => { - console.log('QuizzesPanel Debug:', { ... }); - }, [contentItem, userId, attemptHistory, session]); + useEffect(() => { + if (process.env.NEXT_PUBLIC_DEBUG === 'true') { + console.log('QuizzesPanel Debug:', { /* payload */ }); + } + }, [contentItem, userId, attemptHistory, session]); - console.log('=== handleQuizComplete START ==='); - console.log('Answers:', answers); - console.log('Final Score:', finalScore); - console.log('ContentItem:', contentItem); - console.log('Session:', session); + if (process.env.NEXT_PUBLIC_DEBUG === 'true') { + console.log('=== handleQuizComplete START ==='); + console.log('Final Score:', finalScore); + console.log('ContentItemId:', contentItem?.id); + } ... - console.log('✅ Recording quiz completion:', { ... }); + if (process.env.NEXT_PUBLIC_DEBUG === 'true') { + console.log('Recording quiz completion…', { contentItemId: contentItem.id, finalScore, maxScore }); + } ... - console.log('✅✅ Completion recorded successfully:', result); + if (process.env.NEXT_PUBLIC_DEBUG === 'true') console.log('Completion recorded', result); ... - console.log('=== handleQuizComplete END ==='); + if (process.env.NEXT_PUBLIC_DEBUG === 'true') console.log('=== handleQuizComplete END ===');Additionally, once server enforces identity, you can stop sending userId from the client.
Also applies to: 108-113, 121-153, 164-175
src/lib/auth/auth.config.ts (1)
60-69: Reconsider allowDangerousEmailAccountLinkingEnabling this can let a malicious provider claim the same email to hijack an account. Prefer the default (false) or add strict checks (verified email, provider trust).
- GoogleProvider({ + GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - allowDangerousEmailAccountLinking: true, + // Keep default (false); only link when we explicitly verify ownership. }), - GitHubProvider({ + GitHubProvider({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, - allowDangerousEmailAccountLinking: true, + // Keep default (false) }),Optionally, enforce verified emails in signIn callback before linking.
🧹 Nitpick comments (20)
src/components/quiz/QuizResults.tsx (1)
249-316: Approve the button logic; consider optional refactoring for maintainability.The conditional rendering correctly handles all scenarios:
- Graded + passed: retake to improve
- Graded + failed + retakes allowed: retake to pass
- Graded + failed + no retakes: disabled with guidance
- Ungraded: always allow retakes
The UX is clear with appropriate button states, colors, and supporting messages.
The four levels of nested conditionals (lines 251→254→269→271, 285) are correct but could be harder to maintain. Consider extracting the button logic into a helper function:
const getRetakeButton = () => { if (!isGraded) { return ( <Button onClick={onRestart} size="lg" variant="outline" className="min-w-[200px]"> <RefreshCw className="mr-2 h-5 w-5" /> {isPreviousAttempt ? 'Take Quiz Again' : 'Restart Quiz'} </Button> ); } if (passed) { return ( <div className="space-y-2"> <Button onClick={onRestart} size="lg" variant="outline" className="min-w-[200px]"> <RefreshCw className="mr-2 h-5 w-5" /> Retake to Improve Score </Button> <p className="text-xs text-muted-foreground"> You've already passed, but you can retake to improve your grade </p> </div> ); } if (allowRetakes) { return ( <div className="space-y-2"> <Button onClick={onRestart} size="lg" className="min-w-[200px] bg-amber-600 hover:bg-amber-700"> <RefreshCw className="mr-2 h-5 w-5" /> Retake Quiz to Pass </Button> <p className="text-xs text-muted-foreground"> You need {passingScore}% or higher to pass this quiz </p> </div> ); } return ( <div className="space-y-2"> <Button size="lg" variant="outline" className="min-w-[200px]" disabled> <AlertCircle className="mr-2 h-5 w-5" /> No Retakes Allowed </Button> <p className="text-xs text-destructive"> This quiz does not allow retakes. Contact your instructor for assistance. </p> </div> ); };Then use it in the JSX:
<div className="py-4 text-center space-y-3"> - {/* Show different button text and state based on quiz type and status */} - {isGraded ? ( - <> - ...all the nested logic... - </> - ) : ( - ...ungraded button... - )} + {getRetakeButton()} </div>This flattens the nesting and makes each path clearer while preserving all logic.
convex/schema.ts (1)
181-182: Add usage guidance for latestPassed and adopt new index in queries.
- latestPassed field addition is fine; ensure writers consistently set it alongside passed to avoid divergence.
- Leverage by_courseId (and by_userId_courseId_contentItemId) in any course-level progress scans (e.g., migration/verification) to avoid full-table scans.
Please confirm all write paths (quiz completion, retake) now populate latestPassed and that new queries use withIndex("by_courseId", ...).
Also applies to: 188-190
src/lib/auth/guards.ts (1)
35-36: Type assertion on session.user.id is unchecked.If session.user.id isn’t a Convex Id<"users"> string, queries may fail. Validate format or resolve Convex id during session creation to avoid casts at call sites.
-const userId = session.user.id as Id<"users">; +const userId = session.user.id as unknown as Id<"users">; // temporary +// TODO: Ensure session.user.id is sourced from Convex and validated on session creation.Confirm auth.config.ts sets session.user.id to a Convex GenericId<"users">.
Also applies to: 51-57, 67-73
restart-convex.bat (1)
29-30: Avoid hardcoded path; parameterize project directory.Make the script portable across machines.
-cd /d F:\eca\ecastacademy -npx convex dev +set "PROJECT_DIR=%~dp0" +REM Override PROJECT_DIR if needed: +REM set "PROJECT_DIR=F:\eca\ecastacademy" +cd /d "%PROJECT_DIR%" +npx convex devtest-deployment.js (2)
6-11: window.convex check is brittle.Convex client isn’t usually exposed on window. Prefer a real probe (e.g., call a lightweight API you own) or drop Test 1.
-// Test 1: Check if Convex API is loaded -if (typeof window !== 'undefined' && window.convex) { - console.log('✅ Test 1: Convex client loaded'); -} else { - console.log('❌ Test 1: Convex client NOT loaded'); -} +// Test 1: Ping session endpoint as environment sanity check +fetch('/api/auth/session', { credentials: 'include' }) + .then(() => console.log('✅ Test 1: API reachable')) + .catch(() => console.log('❌ Test 1: API not reachable'));
14-26: Be explicit about cookies.Add credentials: 'include' to avoid surprises with cross-origin setups.
-fetch('/api/auth/session') +fetch('/api/auth/session', { credentials: 'include' })src/lib/convexClient.ts (2)
44-48: Defaulting to admin auth can mask auth bugs.Make admin auth opt-in at call sites that truly need it.
-if (options?.useAdminAuth !== false && deployKey) { +if (options?.useAdminAuth === true && deployKey) { client.setAdminAuth?.(deployKey); -} else if (options?.useAdminAuth === false) { +} else { client.clearAuth?.(); }
25-31: IPv4-only agent may break on IPv6-only environments.Use autoSelectFamily if available.
- globalThis.__convexAgent = new Agent({ - connect: { family: 4 }, - }); + globalThis.__convexAgent = new Agent({ + connect: { autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 1500 }, + });convex/migration.ts (1)
239-360: verifyMigration should be a query.It’s read-only and may run long; use a query and indexes to avoid contention.
- Change mutation(...) to query(...).
- Use withIndex("by_courseId") on progress scans.
convex/completions.ts (1)
649-656: O(n²) lookup across content items — preindex by ID.Inside the per-user loop you linearly find each contentItem. For large courses this degrades quickly. Build a Map once.
Apply:
- const contentItems = allContentItems.flat(); + const contentItems = allContentItems.flat(); + const contentMap = new Map(contentItems.map(ci => [ci._id, ci])); ... - const contentItem = contentItems.find((item) => item._id === summary.contentItemId); + const contentItem = contentMap.get(summary.contentItemId);convex/contentItems.ts (1)
150-151: Avoid blocking the toggle with a full course recalc.recalculateCourseProgressSync can be heavy. Schedule internally to keep UI snappy and add minimal retry/backoff.
Apply:
- await recalculateCourseProgressSync(ctx, { courseId: chapter.courseId }); + await ctx.scheduler.runAfter( + 0, + internal.completions.recalculateCourseProgress, + { courseId: chapter.courseId } + );If synchronous feedback is required, at least debounce/serialize per course.
convex/courses.ts (1)
102-104: Schedule course-wide recalc instead of running synchronously in updateCourse.This mutation may touch many records; run it via scheduler to avoid long tail latencies.
Apply:
- await recalculateCourseProgressSync(ctx, { courseId: id }); + await ctx.scheduler.runAfter( + 0, + internal.completions.recalculateCourseProgress, + { courseId: id } + );convex/certificates.ts (3)
50-57: Avoid N+1 queries for content itemsYou query content items per chapter. Prefer a single index on contentItems by courseId to fetch all in one call for large courses.
Example if index exists:
-const allContentItems = await Promise.all( - chapters.map((chapter) => - ctx.db - .query("contentItems") - .withIndex("by_chapterId", (q) => q.eq("chapterId", chapter._id)) - .collect() - ) -); -const gradedItems = allContentItems.flat().filter((item) => item.isGraded); +const contentItems = await ctx.db + .query("contentItems") + .withIndex("by_courseId", (q) => q.eq("courseId", args.courseId)) + .collect(); +const gradedItems = contentItems.filter((item) => item.isGraded);
89-99: Clamp percentages and guard against zero/invalid maxPointsProtect calculations from out-of-range data and zero/negative weights.
-const maxPoints = item.maxPoints ?? 100; -const bestPercentage = summary.bestPercentage ?? 0; +const rawMax = item.maxPoints ?? 100; +const maxPoints = Math.max(0, rawMax); +const bestPercentage = Math.min(100, Math.max(0, summary.bestPercentage ?? 0)); @@ -const overallGrade = totalPossiblePoints > 0 - ? (totalEarnedPoints / totalPossiblePoints) * 100 - : 0; +const overallGrade = totalPossiblePoints > 0 + ? Math.min(100, Math.max(0, (totalEarnedPoints / totalPossiblePoints) * 100)) + : 0;Also applies to: 116-118
146-147: Store an explicit passedItems count (future-proofing)Setting passedItems to gradedItems.length assumes all passed (true today due to earlier checks). Persist the actual count to survive future rule changes.
- totalGradedItems: gradedItems.length, - passedItems: gradedItems.length, + totalGradedItems: gradedItems.length, + passedItems: gradedItems.length, // all passed now; if rules change, persist a computed countsrc/components/learnspace/learnspace-navbar.tsx (1)
30-33: Simplify userId extractionThe “in” check is redundant given typed session shape.
- const userId = session?.user && "id" in session.user - ? (session.user.id as Id<"users">) - : undefined; + const userId = session?.user?.id as Id<"users"> | undefined;src/lib/auth/auth.config.ts (2)
37-56: Standardize Convex access: use the authenticated wrappers or drop themWrappers are defined but not used consistently (e.g., credentials paths call convex.* directly). Pick one approach for clarity and centralized error handling.
- const existingUser = await convex.query(api.auth.getUserByEmail, { + const existingUser = await authenticatedQuery(api.auth.getUserByEmail, { email, }); @@ - const userId = await convex.mutation(api.auth.createUser, { + const userId = await authenticatedMutation(api.auth.createUser, { email, password: hashedPassword, name: name || undefined, }); @@ - const user = await convex.query(api.auth.getUserByEmail, { email }); + const user = await authenticatedQuery(api.auth.getUserByEmail, { email });Also applies to: 103-121, 130-155
255-271: Avoid querying Convex on every JWT callbackThis runs on most requests and adds latency/cost. Cache user data in the token and refresh periodically (e.g., every 5 minutes) or on specific triggers.
- // Fetch latest user data to get updated role - if (token.id) { + // Periodically refresh user data (every 5 minutes) + const now = Date.now(); + const lastSync = (token as any).userSyncedAt as number | undefined; + const needsSync = !lastSync || now - lastSync > 5 * 60 * 1000; + if (token.id && needsSync) { try { - const currentUser = await convex.query(api.auth.getUserById, { + const currentUser = await convex.query(api.auth.getUserById, { id: token.id as Id<"users">, }); if (currentUser) { token.role = currentUser.role; token.name = currentUser.name; token.picture = currentUser.image; + (token as any).userSyncedAt = now; } } catch (error) { console.error("Error fetching user in JWT callback:", error); } }src/app/api/admin/users/route.ts (2)
10-19: Auth checks look good; minor nitUsing 401 for no session and 403 for admin failures is correct. Consider adding a brief comment explaining admin checks are enforced again in Convex mutations.
26-37: Consistent error messagingGood normalization of 403 vs 500. Optionally return a machine-readable code alongside message for clients.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (33)
CERTIFICATE_BUTTON_IMPLEMENTATION.md(0 hunks)CERTIFICATION_GRADING_FINAL_SUMMARY.md(0 hunks)GRADING_REFACTOR_SUMMARY.md(0 hunks)GRADING_SYSTEM_ANALYSIS.md(0 hunks)QUIZ_GRADING_COMPLETE.md(0 hunks)README.md(0 hunks)UX_IMPROVEMENTS_QUIZ_TRACKING.md(0 hunks)convex/README.md(0 hunks)convex/certificates.ts(8 hunks)convex/completions.ts(10 hunks)convex/contentItems.ts(2 hunks)convex/courses.ts(4 hunks)convex/migration.ts(1 hunks)convex/progress.ts(4 hunks)convex/schema.ts(1 hunks)convex/utils/progressUtils.ts(1 hunks)restart-convex.bat(1 hunks)src/app/api/admin/users/route.ts(5 hunks)src/app/api/ai/generate-text-quiz/route.ts(1 hunks)src/app/api/auth/forgot-password/route.ts(1 hunks)src/app/api/auth/reset-password/route.ts(1 hunks)src/app/api/course/create-from-videos/route.ts(1 hunks)src/app/api/courses/[courseId]/chapters/[chapterId]/route.ts(1 hunks)src/app/api/videos/create/route.ts(1 hunks)src/components/learnspace/learnspace-navbar.tsx(7 hunks)src/components/learnspace/quizzes-panel.tsx(3 hunks)src/components/quiz/QuizResults.tsx(2 hunks)src/lib/auth/auth.config.ts(6 hunks)src/lib/auth/guards.ts(4 hunks)src/lib/convexClient.ts(1 hunks)src/lib/services/courseService.ts(2 hunks)src/lib/services/courseServiceConvex.ts(1 hunks)test-deployment.js(1 hunks)
💤 Files with no reviewable changes (8)
- UX_IMPROVEMENTS_QUIZ_TRACKING.md
- CERTIFICATE_BUTTON_IMPLEMENTATION.md
- GRADING_REFACTOR_SUMMARY.md
- GRADING_SYSTEM_ANALYSIS.md
- README.md
- convex/README.md
- QUIZ_GRADING_COMPLETE.md
- CERTIFICATION_GRADING_FINAL_SUMMARY.md
🧰 Additional context used
🧬 Code graph analysis (19)
convex/contentItems.ts (1)
convex/completions.ts (1)
recalculateCourseProgressSync(736-744)
src/app/api/course/create-from-videos/route.ts (1)
src/lib/convexClient.ts (1)
createConvexClient(37-51)
src/app/api/auth/forgot-password/route.ts (1)
src/lib/convexClient.ts (1)
createConvexClient(37-51)
convex/utils/progressUtils.ts (1)
convex/_generated/dataModel.d.ts (2)
Id(48-49)Doc(30-33)
src/app/api/auth/reset-password/route.ts (1)
src/lib/convexClient.ts (1)
createConvexClient(37-51)
src/lib/services/courseService.ts (2)
src/lib/convexClient.ts (1)
createConvexClient(37-51)src/lib/types/index.ts (1)
ContentItem(39-66)
convex/certificates.ts (1)
convex/utils/progressUtils.ts (1)
summarizeProgressByContentItem(21-87)
src/lib/services/courseServiceConvex.ts (1)
src/lib/convexClient.ts (1)
createConvexClient(37-51)
convex/courses.ts (1)
convex/completions.ts (1)
recalculateCourseProgressSync(736-744)
src/components/learnspace/quizzes-panel.tsx (1)
src/lib/auth/auth.config.ts (1)
session(274-283)
src/lib/auth/guards.ts (3)
src/lib/convexClient.ts (1)
createConvexClient(37-51)src/lib/auth/auth.config.ts (1)
session(274-283)convex/_generated/dataModel.d.ts (1)
Id(48-49)
src/app/api/ai/generate-text-quiz/route.ts (1)
src/lib/convexClient.ts (1)
createConvexClient(37-51)
convex/completions.ts (3)
convex/_generated/dataModel.d.ts (2)
Id(48-49)Doc(30-33)convex/utils/progressUtils.ts (1)
summarizeProgressByContentItem(21-87)convex/_generated/server.d.ts (1)
MutationCtx(121-121)
src/components/learnspace/learnspace-navbar.tsx (3)
src/lib/auth/auth.config.ts (1)
session(274-283)convex/_generated/dataModel.d.ts (1)
Id(48-49)src/lib/utils.ts (1)
cn(4-6)
test-deployment.js (1)
src/lib/auth/auth.config.ts (1)
session(274-283)
src/app/api/courses/[courseId]/chapters/[chapterId]/route.ts (1)
src/lib/convexClient.ts (1)
createConvexClient(37-51)
src/app/api/videos/create/route.ts (1)
src/lib/convexClient.ts (1)
createConvexClient(37-51)
src/lib/auth/auth.config.ts (2)
src/lib/convexClient.ts (1)
createConvexClient(37-51)convex/_generated/dataModel.d.ts (1)
Id(48-49)
src/app/api/admin/users/route.ts (3)
src/lib/convexClient.ts (1)
createConvexClient(37-51)src/lib/auth/auth.config.ts (1)
session(274-283)convex/_generated/dataModel.d.ts (1)
Id(48-49)
🔇 Additional comments (12)
src/components/quiz/QuizResults.tsx (1)
17-18: LGTM! Good defensive programming.The zero-length guard prevents division by zero when calculating the percentage. This is a solid defensive practice.
convex/utils/progressUtils.ts (1)
74-79: latestPassed fallback looks good; keep it consistent.Good use of nullish coalescing to preserve latest intent.
convex/completions.ts (1)
375-385: All required indexes exist in the schema.Both queries have their indexes properly defined:
- Line 375-385: uses
by_userId_contentItemId(defined at schema.ts:187)- Line 444-451: uses
by_userId_courseId(defined at schema.ts:186)Additionally,
by_courseId(schema.ts:189) is also available. No runtime failures will occur due to missing indexes.src/lib/services/courseServiceConvex.ts (1)
7-9: LGTM: Clean refactor to centralized Convex client factory.The migration from direct
ConvexHttpClientinstantiation to thecreateConvexClient()factory improves consistency and centralizes configuration management (including admin auth and environment URL resolution). The functional behavior remains unchanged.src/app/api/auth/forgot-password/route.ts (1)
6-8: LGTM: Consistent adoption of centralized client factory.Same refactor pattern as other routes—replacing direct client instantiation with the factory. This maintains consistency across the codebase.
src/app/api/ai/generate-text-quiz/route.ts (1)
5-7: LGTM: Consistent client factory adoption.src/app/api/auth/reset-password/route.ts (1)
5-7: LGTM: Consistent client factory adoption.src/app/api/courses/[courseId]/chapters/[chapterId]/route.ts (1)
4-6: LGTM: Consistent client factory adoption.src/lib/services/courseService.ts (1)
9-11: LGTM: Consistent client factory adoption.src/app/api/videos/create/route.ts (1)
4-6: LGTM: Consistent client factory adoption.src/app/api/course/create-from-videos/route.ts (1)
4-6: LGTM: Consistent client factory adoption.convex/certificates.ts (1)
26-35: The concurrency vulnerability does not exist in Convex code.Convex mutations run as ACID transactions with serializable isolation, where transactions behave as if executed one-at-a-time. Convex uses optimistic multi-version concurrency control (OCC) with automatic retry on conflicts, so conflicting transactions are retried until they commit without conflict.
Under this model, the check-then-insert pattern is implicitly idempotent:
- Both concurrent executions pass the check against the same initial state.
- The first commits successfully.
- The second detects a read conflict, automatically retries, re-executes the check, finds the now-existing certificate, and returns early without inserting.
The suggested second check and uniqueness constraint are unnecessary; Convex's serializability guarantee already prevents the race condition. The current code is correct as-is.
Likely an incorrect or invalid review comment.
| if ( | ||
| !enrollment || | ||
| (enrollment.status !== "active" && enrollment.status !== "completed") | ||
| ) { | ||
| throw new Error("User is not enrolled in this course"); | ||
| } |
There was a problem hiding this comment.
Authentication/authorization missing for client-supplied userId.
recordCompletion trusts args.userId without verifying the caller’s identity. A malicious client can write progress/attempts for other users.
Apply:
handler: async (ctx, args) => {
- // 1. Validate user exists
- const user = await ctx.db.get(args.userId);
+ // 0. AuthN
+ const identity = await ctx.auth.getUserIdentity();
+ if (!identity) throw new Error("Not authenticated");
+ // 1. Resolve/validate user
+ const user = await ctx.db.get(args.userId);
if (!user) {
throw new Error("User not found");
}
+ // 1b. Ensure caller is the same user (or relax behind explicit admin role if needed)
+ // If your Users doc has email, validate:
+ // const caller = await ctx.db.query("users").withIndex("by_email", q => q.eq("email", identity.email!)).first();
+ // if (!caller || caller._id !== user._id) throw new Error("Forbidden");If you prefer, drop userId from args and derive it from identity for all client‑invoked mutations.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In convex/completions.ts around lines 71 to 76, the mutation currently trusts
args.userId when checking enrollment and can be abused to modify other users'
data; change the implementation to derive the userId from the authenticated
context (eg. ctx.auth or request identity) instead of using a client-supplied
userId, or if you prefer remove userId from args entirely and always use the
caller's id; additionally validate that the resolved userId matches the
enrollment owner and throw an Unauthorized/Forbidden error if it does not before
proceeding with recording completion.
| export const migrateQuizSystemData = mutation({ | ||
| args: {}, | ||
| handler: async (ctx) => { | ||
| console.log("🚀 Starting quiz system migration..."); | ||
|
|
||
| // Get all courses | ||
| const allCourses = await ctx.db.query("courses").collect(); | ||
| console.log(`Found ${allCourses.length} total courses`); | ||
|
|
||
| // Filter certification courses | ||
| const certificationCourses = allCourses.filter(c => c.isCertification); | ||
| console.log(`Found ${certificationCourses.length} certification courses`); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Risk of timeouts/large transactions.
A single mutation looping all courses and patching many docs may exceed Convex limits. Batch per course/page or drive via an action/scheduler that invokes a small, idempotent mutation per batch.
- Add args: courseId?, limit?, cursor?; process in chunks.
- Or implement an action that paginates by withIndex and calls a mutation per batch.
I can draft a batched version using withIndex pagination.
Also applies to: 40-136, 138-217
🤖 Prompt for AI Agents
In convex/migration.ts around lines 24 to 36 (and applicable to ranges 40-136,
138-217), the current single mutation iterates and patches all courses in one go
which risks Convex transaction/time limits; change the migration to be batched:
update the mutation signature to accept args like courseId?: string, limit?:
number, cursor?: string (or batch cursor), and make the handler process only a
single page of courses/records using withIndex pagination or query with a limit
and cursor, apply idempotent updates for that page, return the next cursor (or
finished flag), and provide an accompanying action or scheduler that repeatedly
calls this small mutation until completion; ensure each batch is small, updates
are idempotent, and errors/logging include batch identifiers so the migration
can resume safely.
| .query("progress") | ||
| .filter((q) => q.eq(q.field("courseId"), course._id)) | ||
| .collect(); | ||
|
|
||
| console.log(` Found ${progressRecords.length} progress records`); |
There was a problem hiding this comment.
Avoid full-table scans on progress; use the new index.
Use withIndex("by_courseId") to constrain scans.
-const progressRecords = await ctx.db
- .query("progress")
- .filter((q) => q.eq(q.field("courseId"), course._id))
- .collect();
+const progressRecords = await ctx.db
+ .query("progress")
+ .withIndex("by_courseId", (q) => q.eq("courseId", course._id))
+ .collect();Apply similarly in the non-cert branch below.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .query("progress") | |
| .filter((q) => q.eq(q.field("courseId"), course._id)) | |
| .collect(); | |
| console.log(` Found ${progressRecords.length} progress records`); | |
| const progressRecords = await ctx.db | |
| .query("progress") | |
| .withIndex("by_courseId", (q) => q.eq("courseId", course._id)) | |
| .collect(); | |
| console.log(` Found ${progressRecords.length} progress records`); |
🤖 Prompt for AI Agents
In convex/migration.ts around lines 65 to 69, the query against the "progress"
table performs a full-table scan; update the query to use
withIndex("by_courseId") so it constrains the scan by the courseId index (i.e.,
call .withIndex("by_courseId") before .filter(...)). Make the same change in the
non-cert branch later in the file where progress is queried so both branches use
the by_courseId index and avoid full-table scans.
| * @deprecated Prefer mutations in completions.ts. | ||
| * Keeping the legacy surface for backwards compatibility but it will | ||
| * continue to use its original behaviour. |
There was a problem hiding this comment.
Doc comment contradicts behavior changes.
You state “will continue to use its original behaviour,” but below you change fields (everPassed, bestScore, completedAt logic). Please update the deprecation note to reflect legacy surface with updated internals.
🤖 Prompt for AI Agents
In convex/progress.ts around lines 6 to 8, the deprecation docstring says the
API "will continue to use its original behaviour" which contradicts the
implementation changes to everPassed, bestScore, and completedAt; update the
deprecation note to state that the function provides a legacy public surface but
its internals and some fields (everPassed, bestScore, completedAt logic) have
been updated, briefly list which fields/behaviours changed, and recommend using
mutations in completions.ts for new behavior.
| const newBestScore = Math.max(existingProgress.bestScore ?? 0, percentage); | ||
| const everPassed = (existingProgress.passed ?? false) || passed; | ||
| const shouldComplete = contentItem.isGraded && course.isCertification | ||
| ? (existingProgress.completed || passed) | ||
| : true; | ||
| const now = Date.now(); | ||
|
|
||
| await ctx.db.patch(existingProgress._id, { | ||
| score: args.score, | ||
| maxScore: maxScore, | ||
| percentage: percentage, | ||
| passed: passed, | ||
| passed: everPassed, | ||
| attempts: newAttempts, | ||
| bestScore: newBestScore, | ||
| lastAttemptAt: Date.now(), | ||
| lastAttemptAt: now, |
There was a problem hiding this comment.
🧩 Analysis chain
bestScore is stored as a percentage — ensure all readers treat it as such.
Here bestScore = percentage (0–100). Some consumers (e.g., overall grade/certificate calc) may treat bestScore like raw points, producing incorrect grades for non-100 maxPoints. Align readers or rename the field to bestPercentage for clarity.
Also applies to: 119-125
🏁 Script executed:
#!/bin/bash
# Find where `bestScore` is used as points vs percentage
rg -n -C2 -P '\bbestScore\b' convex --type=tsLength of output: 5748
Fix total earned points calculations treating bestScore as percentage, not raw points.
The verification confirms the concern: bestScore is consistently stored as a percentage (0–100) throughout the codebase, but in grade/certificate calculations it's summed as raw points.
Locations requiring fixes:
convex/progress.ts:339—totalEarnedPointsreduction sumsbestScoredirectlyconvex/progress.ts:568—totalEarnedPointsreduction sumsbestScoredirectlyconvex/debug.ts:60— same issue in debug totals
For multi-item courses, this inflates totals (e.g., 5 items with bestScore=100 each yields 500 instead of normalized points). Either divide by 100 or convert bestScore to raw points at assignment time (e.g., bestScore: (percentage / 100) * maxScore).
🤖 Prompt for AI Agents
In convex/progress.ts (around lines 87–101) and also update reductions at
convex/progress.ts lines ~339 and ~568 and convex/debug.ts line ~60: bestScore
is currently stored/treated as a percentage (0–100) but later summed as raw
points; fix by storing bestScore as raw points when updating progress (compute
bestScore = (percentage / 100) * maxScore) or, if you prefer minimal change,
update the totalEarnedPoints reductions to convert the stored percentage to raw
points during summation (use (bestScore / 100) * corresponding maxScore for each
item); ensure consistency across all three locations so totals use raw points
not percentages.
| {certificateAvailable ? ( | ||
| <Button | ||
| asChild | ||
| size="sm" | ||
| className={cn( | ||
| "text-white shadow-lg", | ||
| certificateIssued | ||
| ? "bg-blue-600 hover:bg-blue-700" | ||
| : "bg-green-600 hover:bg-green-700" | ||
| )} | ||
| > | ||
| <Link href="/dashboard/certificates"> | ||
| <Award className="h-4 w-4 mr-2" /> | ||
| {certificateIssued ? "View Certificate" : "Get Certificate"} | ||
| </Link> | ||
| </Button> | ||
| ) : ( | ||
| <Button | ||
| size="sm" | ||
| disabled | ||
| className="cursor-not-allowed" | ||
| variant="outline" | ||
| > | ||
| <Award className="h-4 w-4 mr-2 opacity-50" /> | ||
| Get Certificate | ||
| </Button> | ||
| )} |
There was a problem hiding this comment.
Let users view already-issued certificates even if eligibility later drops
Currently the button is disabled when eligibleForCertificate is false, even if hasCertificate is true. Issued certificates should always be viewable.
Apply:
- {certificateAvailable ? (
+ {certificateIssued || certificateAvailable ? (
<Button
asChild
size="sm"
className={cn(
"text-white shadow-lg",
- certificateIssued
+ certificateIssued
? "bg-blue-600 hover:bg-blue-700"
: "bg-green-600 hover:bg-green-700"
)}
>
<Link href="/dashboard/certificates">
<Award className="h-4 w-4 mr-2" />
- {certificateIssued ? "View Certificate" : "Get Certificate"}
+ {certificateIssued ? "View Certificate" : "Get Certificate"}
</Link>
</Button>
) : (And adjust tooltip to reflect the same condition:
- {certificateAvailable ? (
- certificateIssued ? (
+ {certificateIssued ? (
<div className="space-y-1">
<p className="font-semibold text-green-600">🎉 Certificate Ready</p>
<p className="text-sm">You've already earned this certificate.</p>
<p className="text-xs text-muted-foreground">Click to view and download.</p>
</div>
- ) : (
+ ) : certificateAvailable ? (
<div className="space-y-1">
<p className="font-semibold text-green-600">🎉 Congratulations!</p>
<p className="text-sm">All requirements met. Download your certificate.</p>
</div>
) : (Also applies to: 250-279
🤖 Prompt for AI Agents
In src/components/learnspace/learnspace-navbar.tsx around lines 221 to 247 (and
apply same change to 250-279), the current UI disables the certificate button
whenever eligibleForCertificate is false even if the user already has a
certificate; update the enable/disable logic so the button is enabled when
either hasCertificate (certificateIssued) is true OR eligibleForCertificate is
true (e.g., use const canOpenCertificate = certificateIssued ||
certificateAvailable and use that for the conditional), ensure the button text
shows "View Certificate" when certificateIssued is true and "Get Certificate"
otherwise, and only render the disabled outline button when neither condition is
true; also update any tooltip text to reflect that issued certificates are
always viewable while eligibility controls issuance.
| console.log('🔍 [AUTH CONFIG] Convex URL:', convexUrl); | ||
| console.log('🔍 [AUTH CONFIG] Has Deploy Key:', !!deployKey); | ||
| console.log('🔍 [AUTH CONFIG] Deploy Key preview:', deployKey ? deployKey.substring(0, 30) + '...' : 'none'); | ||
|
|
There was a problem hiding this comment.
Stop logging secrets (deploy key) to server logs
Leaking even a prefix is a security risk. Remove or guard behind dev-only with masking.
-console.log('🔍 [AUTH CONFIG] Convex URL:', convexUrl);
-console.log('🔍 [AUTH CONFIG] Has Deploy Key:', !!deployKey);
-console.log('🔍 [AUTH CONFIG] Deploy Key preview:', deployKey ? deployKey.substring(0, 30) + '...' : 'none');
+if (process.env.NODE_ENV === "development") {
+ console.log('🔍 [AUTH CONFIG] Convex URL configured');
+ console.log('🔍 [AUTH CONFIG] Deploy Key present:', Boolean(deployKey));
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log('🔍 [AUTH CONFIG] Convex URL:', convexUrl); | |
| console.log('🔍 [AUTH CONFIG] Has Deploy Key:', !!deployKey); | |
| console.log('🔍 [AUTH CONFIG] Deploy Key preview:', deployKey ? deployKey.substring(0, 30) + '...' : 'none'); | |
| if (process.env.NODE_ENV === "development") { | |
| console.log('🔍 [AUTH CONFIG] Convex URL configured'); | |
| console.log('🔍 [AUTH CONFIG] Deploy Key present:', Boolean(deployKey)); | |
| } |
🤖 Prompt for AI Agents
In src/lib/auth/auth.config.ts around lines 30 to 33, the code logs the
deployKey (even a prefix) to server logs; remove direct logging of secrets and
either (a) only log a non-secret boolean/presence indicator or masked value, or
(b) restrict any sensitive logging to development only (check NODE_ENV ===
'development') and mask the key (e.g., show only a fixed small suffix/prefix or
replace with '***') before logging; also prefer using the app logger instead of
console.log. Implement one of these fixes and remove the existing console.log
that prints deployKey or its preview.
| import { createConvexClient } from "@/lib/convexClient"; | ||
|
|
||
| const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); | ||
| const convex = createConvexClient(); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
Mark this module server-only and avoid default admin auth.
This file imports a Node-only agent via createConvexClient; prevent accidental client bundling and avoid using admin auth by default in guards.
Apply:
+import "server-only";
-import { createConvexClient } from "@/lib/convexClient";
-const convex = createConvexClient();
+import { createConvexClient } from "@/lib/convexClient";
+const convex = createConvexClient({ useAdminAuth: false });Run the scan below to ensure no client components import createConvexClient:
🏁 Script executed:
#!/bin/bash
# Find any 'use client' files importing convexClient
rg -nP -C2 $'^(?:"use client"|\'use client\')' --type=ts --type=tsx \
| awk -F: '{print $1}' | sort -u | while read f; do
rg -nP '\bfrom\s+[\'"]@/lib/convexClient[\'"]' "$f"
doneLength of output: 68
I'll fix the shell script syntax error and re-run the verification:
🏁 Script executed:
#!/bin/bash
# Find client components that import createConvexClient
rg -l '"use client"' --type=ts --type=tsx | while read file; do
if rg -q '@/lib/convexClient' "$file"; then
echo "Found in: $file"
rg -n '@/lib/convexClient' "$file"
fi
doneLength of output: 27
🏁 Script executed:
#!/bin/bash
# Find client components that import createConvexClient
rg -l '"use client"' -g '*.ts' -g '*.tsx' | while read file; do
if rg -q '@/lib/convexClient' "$file"; then
echo "Found in: $file"
rg -n '@/lib/convexClient' "$file"
fi
doneLength of output: 0
🏁 Script executed:
#!/bin/bash
# Check guards.ts content and convexClient implementation
echo "=== guards.ts ===" && cat src/lib/auth/guards.ts && \
echo -e "\n=== convexClient.ts ===" && cat src/lib/convexClient.tsLength of output: 2980
Add "server-only" directive and disable admin auth by default.
The file lacks the "server-only" import to prevent accidental client-side bundling, and the guard functions should not use admin authentication. The createConvexClient() factory currently enables admin auth by default when CONVEX_DEPLOY_KEY is available—this should be disabled for user-scoped queries in guards.
Apply:
+import "server-only";
import { auth } from "./auth.config";
import { redirect } from "next/navigation";
import { api } from "../../../convex/_generated/api";
import { Id } from "../../../convex/_generated/dataModel";
import { createConvexClient } from "@/lib/convexClient";
-const convex = createConvexClient();
+const convex = createConvexClient({ useAdminAuth: false });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createConvexClient } from "@/lib/convexClient"; | |
| const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); | |
| const convex = createConvexClient(); | |
| import "server-only"; | |
| import { createConvexClient } from "@/lib/convexClient"; | |
| const convex = createConvexClient({ useAdminAuth: false }); | |
🤖 Prompt for AI Agents
In src/lib/auth/guards.ts around lines 5 to 8, add the server-only directive and
ensure the Convex client used by guards does not run with admin auth: import
"server-only" at the top of the file, and call createConvexClient with the
option that disables admin/admin-key authentication (so the client performs
user-scoped queries only) instead of the current default that enables admin auth
when CONVEX_DEPLOY_KEY is present.
| import { ConvexHttpClient } from "convex/browser"; | ||
| import { Agent } from "undici"; | ||
|
|
||
| const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; | ||
| const deployKey = process.env.CONVEX_DEPLOY_KEY; | ||
|
|
||
| if (!convexUrl) { | ||
| throw new Error("NEXT_PUBLIC_CONVEX_URL is not configured."); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
Enforce server-only to prevent bundling undici in the client.
This module imports undici’s Agent; mark server-only.
+import "server-only";
import { ConvexHttpClient } from "convex/browser";
import { Agent } from "undici";
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
const deployKey = process.env.CONVEX_DEPLOY_KEY;Run the scan in the guards.ts comment to ensure no client components import this module.
---
🏁 Script executed:
```shell
#!/bin/bash
# Verify the file exists and show current content
cat src/lib/convexClient.ts
Length of output: 1247
🏁 Script executed:
#!/bin/bash
# Search for imports of convexClient.ts from client components or pages
rg -nP '(?:from\s+["\'].*convexClient|import\s+.*convexClient)' --type=ts --type=tsx -C2Length of output: 175
🏁 Script executed:
#!/bin/bash
# Check if guards.ts exists and look for scanning utility
fd -t f guards.ts -x cat {}Length of output: 1689
🏁 Script executed:
#!/bin/bash
# Search for "server-only" usage pattern in the codebase to understand convention
rg -n 'server-only' --type=ts --type=tsx -C2 | head -40Length of output: 27
🏁 Script executed:
#!/bin/bash
# Check if undici is in package.json and what it's used for
fd -t f package.json -x cat {} | grep -A5 -B5 undici || echo "No package.json or undici not found in this context"Length of output: 51
🏁 Script executed:
#!/bin/bash
# Simpler search: find any imports of convexClient
rg -n "convexClient" --type=ts -C2Length of output: 4707
🏁 Script executed:
#!/bin/bash
# Look for client components that might import convexClient
fd -t f -e ts -e tsx | xargs grep -l "use client" | head -20Length of output: 746
🏁 Script executed:
#!/bin/bash
# Check app directory structure to understand server vs client organization
fd -t d -E node_modules -E ".next" | grep -E "(app|src)" | head -30Length of output: 794
🏁 Script executed:
#!/bin/bash
# Search for server-only imports in the codebase
rg -n "server-only" --type=tsLength of output: 0
🏁 Script executed:
#!/bin/bash
# Look at package.json to verify undici dependency
fd -t f -name "package.json" -x catLength of output: 230
Add "server-only" import to prevent undici from bundling in client builds.
The file imports undici (Node.js-only) without marking the module as server-only. All current imports are from server-side code (API routes, auth, services), but without the marker, a client component could accidentally import this and fail. Add the import at the top:
+import "server-only";
import { ConvexHttpClient } from "convex/browser";
import { Agent } from "undici";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { ConvexHttpClient } from "convex/browser"; | |
| import { Agent } from "undici"; | |
| const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; | |
| const deployKey = process.env.CONVEX_DEPLOY_KEY; | |
| if (!convexUrl) { | |
| throw new Error("NEXT_PUBLIC_CONVEX_URL is not configured."); | |
| } | |
| import "server-only"; | |
| import { ConvexHttpClient } from "convex/browser"; | |
| import { Agent } from "undici"; | |
| const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; | |
| const deployKey = process.env.CONVEX_DEPLOY_KEY; | |
| if (!convexUrl) { | |
| throw new Error("NEXT_PUBLIC_CONVEX_URL is not configured."); | |
| } |
🤖 Prompt for AI Agents
src/lib/convexClient.ts lines 1-9: this file imports the Node-only 'undici'
Agent but doesn't mark the module as server-only, which can cause client builds
to accidentally bundle it; add the server-only marker by inserting import
"server-only"; as the first line of the file (before any other imports) to
ensure this module and its undici dependency are only used on the server.
| isCertification: chapter.course.isCertification ?? false, | ||
| passingGrade: chapter.course.passingGrade ?? undefined, | ||
| }, | ||
| contentItems: chapter.contentItems, // Include content items! | ||
| contentItems: chapter.contentItems?.map((item: ContentItem & { allowRetakes?: boolean }) => ({ | ||
| ...item, | ||
| isGraded: item.isGraded ?? false, | ||
| allowRetakes: item.allowRetakes ?? true, | ||
| maxPoints: item.maxPoints ?? undefined, | ||
| passingScore: item.passingScore ?? undefined, | ||
| })), |
There was a problem hiding this comment.
🧩 Analysis chain
LGTM: Certification fields added with sensible defaults.
The new certification-related fields (isCertification, passingGrade, isGraded, allowRetakes, maxPoints, passingScore) are added with appropriate fallback values using nullish coalescing. The defaults align well with expected behavior.
Verify that these field defaults match the backend schema definitions and business requirements:
🏁 Script executed:
#!/bin/bash
# Search for schema definitions and defaults for certification fields
echo "=== Searching for schema definitions ==="
rg -n "isCertification|passingGrade|isGraded|allowRetakes|maxPoints|passingScore" convex/schema.ts -A2 -B2
echo -e "\n=== Searching for field usage in convex functions ==="
rg -n "isCertification|passingGrade" convex/courses.ts -A2 -B2
echo -e "\n=== Searching for grading field usage ==="
rg -n "isGraded|allowRetakes|maxPoints|passingScore" convex/contentItems.ts -A2 -B2Length of output: 5871
Update frontend defaults to match backend business logic.
The review comment's approval is premature. The frontend defaults do not align with backend schema and business requirements:
passingGrade: Frontend defaults toundefined, but backend defaults to70isGraded: Frontend defaults tofalse, but backend auto-determines this based on course certification status and content type (not a simple false default)maxPoints: Frontend defaults toundefined, but backend defaults to100whenisGradedis truepassingScore: Frontend defaults toundefined, but backend defaults tocourse.passingGrade ?? 70whenisGradedis true
The frontend code should replicate the backend's logic-based defaults, particularly for isGraded and passingScore, to ensure consistency between what users see and what persists in the database.
|
Note Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
Note Docstrings generation - SUCCESS |
Docstrings generation was requested by @Avinash1286. * #4 (comment) The following files were modified: * `convex/completions.ts` * `convex/utils/progressUtils.ts` * `src/app/api/admin/users/route.ts` * `src/app/api/ai/generate-text-quiz/route.ts` * `src/app/api/auth/forgot-password/route.ts` * `src/app/api/auth/reset-password/route.ts` * `src/app/api/course/create-from-videos/route.ts` * `src/app/api/courses/[courseId]/chapters/[chapterId]/route.ts` * `src/app/api/videos/create/route.ts` * `src/components/learnspace/learnspace-navbar.tsx` * `src/components/learnspace/quizzes-panel.tsx` * `src/lib/auth/auth.config.ts` * `src/lib/auth/guards.ts` * `src/lib/convexClient.ts` * `src/lib/services/courseService.ts`
|
✅ UTG Post-Process Complete No new issues were detected in the generated code and all check runs have completed. The unit test generation process has completed successfully. |
|
Creating a PR to put the unit tests in... The changes have been created in this pull request: View PR |
Summary by CodeRabbit
Release Notes
New Features
Improvements