Skip to content

Fix/bughunt konstanty#14

Open
Tiin0 wants to merge 3 commits intoJavaScript-Mastery-Pro:mainfrom
Tiin0:fix/bughunt-Konstanty
Open

Fix/bughunt konstanty#14
Tiin0 wants to merge 3 commits intoJavaScript-Mastery-Pro:mainfrom
Tiin0:fix/bughunt-Konstanty

Conversation

@Tiin0
Copy link
Copy Markdown

@Tiin0 Tiin0 commented Apr 19, 2026

Summary by CodeRabbit

  • Bug Fixes
    • Improved error handling and security for API responses
    • Fixed timing issue with grade database updates
    • Enhanced precision of grade averages in dashboard visualizations

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

Three files updated to improve API error handling consistency, refine user profile resolution logic with explicit fallbacks, and increase precision of grade average calculations in the radar chart from whole numbers to two-decimal fixed format.

Changes

Cohort / File(s) Summary
API Error Handling & Validation
app/api/grades/route.ts
Standardized error responses to return fixed { error: 'Internal server error' } with HTTP 500; updated POST validation to return raw parsed error instead of flattened; added await to findOneAndUpdate operation to ensure synchronous response handling.
User Profile Resolution
app/api/profile/route.ts
Enhanced GET handler to immediately resolve userId from auth() and session with 401 fallback; added explicit 404 for missing user data in teacher lookup; changed name derivation from fullName to concatenated firstName and lastName with trimming and Anonymous fallback; replaced toObject() with JSON.parse(JSON.stringify(...)) for teacher document cloning; updated error logging to log error object directly.
Grade Calculation Precision
app/dashboard/grades/GradesClient.tsx
Updated radarData average score calculation to use two-decimal precision (toFixed(2)) instead of integer rounding for subject averages in radar chart.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hops of joy through error flows,
User paths now crystal-clear,
Numbers dancing, two decimals show,
APIs safer, profiles sincere!
A rabbit's code, refined and near.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix/bughunt konstanty' is vague and does not clearly describe the specific changes made in the pull request. It lacks meaningful context about what bugs were fixed or what systems were modified. Revise the title to clearly summarize the main changes, such as: 'Fix error handling in grades and profile API routes' or 'Refactor API error responses and add null safety checks'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/api/grades/route.ts (1)

76-80: ⚠️ Potential issue | 🟡 Minor

Good catch adding await to findOneAndUpdate.

Without the await, the handler previously returned a pending Query as the JSON body and DB/pre-hook errors escaped the try/catch. One follow-up: the pre('findOneAndUpdate') hook in models/Grade.ts throws "marks must be less than or equal to maxMarks" — with await in place this will now be caught and collapsed to a generic 500. The Zod refine on lines 14–20 already covers this case when maxMarks is present, but note that maxMarks is optional in the schema and const max = data.maxMarks! on line 73 uses a non-null assertion; if a client omits maxMarks, max is undefined, calcGrade(data.marks, undefined) yields NaN, and the Zod refine short-circuits on !data.maxMarks, so the bad value silently reaches the DB. Consider requiring maxMarks (or defaulting it) rather than asserting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/grades/route.ts` around lines 76 - 80, The handler currently asserts
maxMarks with const max = data.maxMarks! and calls calcGrade(data.marks, max)
which can produce NaN if maxMarks is omitted; update the Zod input schema to
require or provide a default for maxMarks (modify the schema where the Zod
refine is defined around lines 14–20) OR change the handler to derive max safely
(e.g. use a default or throw a validation error before calling calcGrade) and
remove the non-null assertion; ensure Grade.findOneAndUpdate usage remains
awaited and that validation prevents invalid marks from reaching the
pre('findOneAndUpdate') hook in models/Grade.ts.
🧹 Nitpick comments (2)
app/api/profile/route.ts (1)

38-38: Avoid JSON round-tripping Mongoose documents.

JSON.parse(JSON.stringify(newTeacher)) works as a response-normalization shortcut, but it discards runtime types before the response boundary. Prefer newTeacher.toObject() or return a lean document from the create/upsert flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/profile/route.ts` at line 38, Replace the JSON round-trip on the
Mongoose document: instead of assigning teacher =
JSON.parse(JSON.stringify(newTeacher)), call the Mongoose instance method to
convert the document to a plain object (e.g. use newTeacher.toObject()) or
ensure your create/upsert returns a lean document; update the assignment to use
newTeacher.toObject() (or the lean result) so runtime types and Mongoose
behavior are preserved.
app/dashboard/grades/GradesClient.tsx (1)

211-218: Two-decimal precision here is partially cosmetic and inconsistent with activeAvg.

A couple of notes on this change:

  1. The inner pct(g.marks, g.maxMarks) (line 58–60) already applies Math.round(...), so the averaged values being summed are whole-number integers. The toFixed(2) therefore only exposes fractional parts introduced by the division sum / sg.length, not the underlying precision of each subject's percentage. If the intent was truly higher precision, compute the raw ratio here instead of calling pct.
  2. activeAvg (lines 340–346) still uses Math.round(...) for the stats bar, so the radar chart and the "Avg: X%" stat will disagree for the same subject. Consider applying the same rounding policy in both places.
  3. Minor: the reassignment pattern can collapse to a single const.
♻️ Suggested simplification (chart-only precision)
-        let avg =
-          sg.length > 0
-            ? 
-            sg.reduce((s, g) => s + pct(g.marks, g.maxMarks), 0) / sg.length
-            : 0
-
-        avg = Number(avg.toFixed(2));
-        
+        const avg =
+          sg.length > 0
+            ? Number(
+                (
+                  sg.reduce((s, g) => s + (g.marks / g.maxMarks) * 100, 0) /
+                  sg.length
+                ).toFixed(2)
+              )
+            : 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/dashboard/grades/GradesClient.tsx` around lines 211 - 218, The avg
calculation uses pct(...) (which applies Math.round) then divides, causing
inconsistent precision vs activeAvg and a pointless toFixed reassign; instead
compute the raw percentage ratio from each subject (use g.marks / g.maxMarks *
100 rather than pct(g.marks, g.maxMarks)), average those raw values, then apply
the desired rounding policy consistently with activeAvg (either two-decimal for
chart-only or Math.round for both); also collapse the reassignment into a single
const avg, updating references to avg, pct, and activeAvg accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/grades/route.ts`:
- Around line 69-70: The validation response currently returns the raw ZodError
(parsed.error) from the GradeSchema safeParse; change the handler to return the
flattened error shape instead by calling parsed.error.flatten() and pass that
flattened result into NextResponse.json so the response uses
formErrors/fieldErrors rather than exposing Zod internals (update the code
around the GradeSchema safeParse and the NextResponse.json error return).

In `@app/api/profile/route.ts`:
- Around line 10-15: The code currently trusts queryUserId before confirming
authentication; call auth() first and if session is missing immediately return
401, then derive userId only from session.userId (do not allow queryUserId to
override it). Locate the auth() call and the variables queryUserId, session and
userId in route.ts and change the assignment so userId = session.userId (ignore
or validate queryUserId only after verifying the session and explicit
authorization, e.g. admin check in a separate branch).
- Around line 21-38: The current findOne → create flow can race and hit the
unique clerkId constraint; replace it with an atomic upsert or
retry-on-duplicate pattern: use Teacher.findOneAndUpdate({ clerkId: userId }, {
$setOnInsert: { clerkId: userId, name: ..., email: ..., department: '',
subjects: [] } }, { upsert: true, new: true, setDefaultsOnInsert: true }) (or
wrap Teacher.create in a try/catch that retries a findOne after E11000
duplicate-key error) so that the logic around Teacher.findOne, Teacher.create
and assignment to the teacher variable becomes atomic and race-safe while still
using currentUser() to populate defaults.

---

Outside diff comments:
In `@app/api/grades/route.ts`:
- Around line 76-80: The handler currently asserts maxMarks with const max =
data.maxMarks! and calls calcGrade(data.marks, max) which can produce NaN if
maxMarks is omitted; update the Zod input schema to require or provide a default
for maxMarks (modify the schema where the Zod refine is defined around lines
14–20) OR change the handler to derive max safely (e.g. use a default or throw a
validation error before calling calcGrade) and remove the non-null assertion;
ensure Grade.findOneAndUpdate usage remains awaited and that validation prevents
invalid marks from reaching the pre('findOneAndUpdate') hook in models/Grade.ts.

---

Nitpick comments:
In `@app/api/profile/route.ts`:
- Line 38: Replace the JSON round-trip on the Mongoose document: instead of
assigning teacher = JSON.parse(JSON.stringify(newTeacher)), call the Mongoose
instance method to convert the document to a plain object (e.g. use
newTeacher.toObject()) or ensure your create/upsert returns a lean document;
update the assignment to use newTeacher.toObject() (or the lean result) so
runtime types and Mongoose behavior are preserved.

In `@app/dashboard/grades/GradesClient.tsx`:
- Around line 211-218: The avg calculation uses pct(...) (which applies
Math.round) then divides, causing inconsistent precision vs activeAvg and a
pointless toFixed reassign; instead compute the raw percentage ratio from each
subject (use g.marks / g.maxMarks * 100 rather than pct(g.marks, g.maxMarks)),
average those raw values, then apply the desired rounding policy consistently
with activeAvg (either two-decimal for chart-only or Math.round for both); also
collapse the reassignment into a single const avg, updating references to avg,
pct, and activeAvg accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e15218e5-25fc-40bf-b522-358aa23f9b54

📥 Commits

Reviewing files that changed from the base of the PR and between c71cbbf and 12b0bc4.

📒 Files selected for processing (3)
  • app/api/grades/route.ts
  • app/api/profile/route.ts
  • app/dashboard/grades/GradesClient.tsx

Comment thread app/api/grades/route.ts
Comment on lines 69 to +70
const parsed = GradeSchema.safeParse(body)
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })
if (!parsed.success) return NextResponse.json({ error: parsed.error }, { status: 400 })
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Zod ZodError toJSON vs flatten() vs format() recommended error response shape

💡 Result:

For ZodError error handling in Zod (stable v4 as of 2026), use these methods to shape responses: - error.toJSON: Serializes the ZodError to JSON, producing {name: 'ZodError', issues: [...]}, where issues is the raw array of ZodIssue objects (code, path, message, etc.). Suitable for simple API responses but retains full structured details. Note: JSON.stringify(error) uses this via the Error prototype's message getter, which stringifies issues. - error.flatten or z.flattenError(error): Recommended for flat schemas (most common, e.g., forms, top-level API objects). Returns {formErrors: string[], fieldErrors: {[key: string]: string[]}}, grouping top-level errors and per-field error arrays. Ideal for client-side form error display or simple API field-error responses. - error.format or z.formatError(error): Deprecated; use z.treeifyError(error) instead. treeifyError produces a nested object mirroring the schema structure: {errors: string[], properties: {...nested errors...}}. Best for deeply nested schemas. Recommended error response shape depends on use case: - For API responses (e.g., HTTP 400), use flattened shape for simplicity: {formErrors: [...], fieldErrors: {field1: [...], ...}}. Easy to parse, matches common patterns (React Hook Form, etc.), avoids deep nesting. - Raw error.toJSON if full issue details needed (codes, expected types). - treeifyError only for complex nested data. Official docs emphasize flatten for flat schemas (majority) and treeify for nested. Examples show flatten for form handling. GitHub issue confirms JSON serialization via issues array, with caveats for client reconstruction (use new ZodError(issues)). Examples: const flattened = error.flatten; // {formErrors: ['...'], fieldErrors: {username: ['...']}} res.status(400).json(flattened); // API response JSON.stringify(error); // '{"name":"ZodError","issues":[...]}'

Citations:


🏁 Script executed:

cat -n app/api/grades/route.ts | head -80 | tail -20

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 903


🏁 Script executed:

git log --oneline -5 -- app/api/grades/route.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 121


🏁 Script executed:

rg "NextResponse.json.*error" app/api/grades/route.ts -A 1

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 629


🏁 Script executed:

rg "GradeSchema" app/api/grades/route.ts -B 5 -A 10

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1257


🏁 Script executed:

git diff HEAD~ app/api/grades/route.ts 2>/dev/null || echo "No previous version"

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 103


🏁 Script executed:

fd "GradeSchema" --type f --exec grep -l "GradeSchema" {} \;

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


Validation error response uses raw ZodError instead of flattened shape.

Line 70 returns parsed.error directly, which serializes to {name: 'ZodError', issues: [...]} and exposes Zod internals. This breaks consistency: every other error in this file uses a plain string ({ error: 'string' }), and this ZodError shape will confuse clients expecting a uniform error contract.

Since GradeSchema is flat (no nested objects), use .flatten() to produce {formErrors: string[], fieldErrors: {field: string[]}}, which is both client-friendly and the documented Zod best practice for flat schemas.

♻️ Suggested fix
-    if (!parsed.success) return NextResponse.json({ error: parsed.error }, { status: 400 })
+    if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })
📝 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.

Suggested change
const parsed = GradeSchema.safeParse(body)
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })
if (!parsed.success) return NextResponse.json({ error: parsed.error }, { status: 400 })
const parsed = GradeSchema.safeParse(body)
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/grades/route.ts` around lines 69 - 70, The validation response
currently returns the raw ZodError (parsed.error) from the GradeSchema
safeParse; change the handler to return the flattened error shape instead by
calling parsed.error.flatten() and pass that flattened result into
NextResponse.json so the response uses formErrors/fieldErrors rather than
exposing Zod internals (update the code around the GradeSchema safeParse and the
NextResponse.json error return).

Comment thread app/api/profile/route.ts
Comment on lines +10 to +15
// 1. Identificazione utente
const session = await auth()
const userId = queryUserId || session?.userId

if (!userId) {
const session = await auth()
userId = session.userId
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not let queryUserId bypass authentication or override the session user.

Line 12 trusts queryUserId before confirming an authenticated session. This allows /api/profile?userId=... to read another profile unauthenticated if it exists, and can also create mismatched records later by combining a supplied clerkId with the current Clerk user data.

🔒 Proposed fix
-  const session = await auth()
-  const userId = queryUserId || session?.userId
+  const { userId: sessionUserId } = await auth()

-  if (!userId) {
+  if (!sessionUserId) {
     return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
   }
+
+  if (queryUserId && queryUserId !== sessionUserId) {
+    return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
+  }
+
+  const userId = queryUserId ?? sessionUserId
📝 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.

Suggested change
// 1. Identificazione utente
const session = await auth()
const userId = queryUserId || session?.userId
if (!userId) {
const session = await auth()
userId = session.userId
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// 1. Identificazione utente
const { userId: sessionUserId } = await auth()
if (!sessionUserId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
if (queryUserId && queryUserId !== sessionUserId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
const userId = queryUserId ?? sessionUserId
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/profile/route.ts` around lines 10 - 15, The code currently trusts
queryUserId before confirming authentication; call auth() first and if session
is missing immediately return 401, then derive userId only from session.userId
(do not allow queryUserId to override it). Locate the auth() call and the
variables queryUserId, session and userId in route.ts and change the assignment
so userId = session.userId (ignore or validate queryUserId only after verifying
the session and explicit authorization, e.g. admin check in a separate branch).

Comment thread app/api/profile/route.ts
Comment on lines 21 to +38
let teacher = await Teacher.findOne({ clerkId: userId }).lean()

if (!teacher) {
const clerkUser = await currentUser()
const created = await Teacher.create({

if (!clerkUser) {
return NextResponse.json({ error: 'User data not found' }, { status: 404 })
}

const newTeacher = await Teacher.create({
clerkId: userId,
name: clerkUser?.fullName ?? '',
email: clerkUser?.emailAddresses[0]?.emailAddress ?? '',
name: `${clerkUser.firstName ?? ''} ${clerkUser.lastName ?? ''}`.trim() || 'Anonymous',
email: clerkUser.emailAddresses[0]?.emailAddress ?? '',
department: '',
subjects: [],
})
teacher = created.toObject()

teacher = JSON.parse(JSON.stringify(newTeacher))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make first-profile creation atomic.

The findOnecreate sequence can race for a new user. With the unique clerkId constraint in models/Teacher.ts, concurrent first GETs can produce a duplicate-key error and return 500.

Prefer an atomic upsert or handle duplicate-key retry:

🛠️ Proposed direction
-      const newTeacher = await Teacher.create({
-        clerkId: userId,
-        name: `${clerkUser.firstName ?? ''} ${clerkUser.lastName ?? ''}`.trim() || 'Anonymous',
-        email: clerkUser.emailAddresses[0]?.emailAddress ?? '',
-        department: '',
-        subjects: [],
-      })
-      
-      teacher = JSON.parse(JSON.stringify(newTeacher))
+      teacher = await Teacher.findOneAndUpdate(
+        { clerkId: userId },
+        {
+          $setOnInsert: {
+            clerkId: userId,
+            name: `${clerkUser.firstName ?? ''} ${clerkUser.lastName ?? ''}`.trim() || 'Anonymous',
+            email: clerkUser.emailAddresses[0]?.emailAddress ?? '',
+            department: '',
+            subjects: [],
+          },
+        },
+        { new: true, upsert: true, setDefaultsOnInsert: true },
+      ).lean()
📝 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.

Suggested change
let teacher = await Teacher.findOne({ clerkId: userId }).lean()
if (!teacher) {
const clerkUser = await currentUser()
const created = await Teacher.create({
if (!clerkUser) {
return NextResponse.json({ error: 'User data not found' }, { status: 404 })
}
const newTeacher = await Teacher.create({
clerkId: userId,
name: clerkUser?.fullName ?? '',
email: clerkUser?.emailAddresses[0]?.emailAddress ?? '',
name: `${clerkUser.firstName ?? ''} ${clerkUser.lastName ?? ''}`.trim() || 'Anonymous',
email: clerkUser.emailAddresses[0]?.emailAddress ?? '',
department: '',
subjects: [],
})
teacher = created.toObject()
teacher = JSON.parse(JSON.stringify(newTeacher))
let teacher = await Teacher.findOne({ clerkId: userId }).lean()
if (!teacher) {
const clerkUser = await currentUser()
if (!clerkUser) {
return NextResponse.json({ error: 'User data not found' }, { status: 404 })
}
teacher = await Teacher.findOneAndUpdate(
{ clerkId: userId },
{
$setOnInsert: {
clerkId: userId,
name: `${clerkUser.firstName ?? ''} ${clerkUser.lastName ?? ''}`.trim() || 'Anonymous',
email: clerkUser.emailAddresses[0]?.emailAddress ?? '',
department: '',
subjects: [],
},
},
{ new: true, upsert: true, setDefaultsOnInsert: true },
).lean()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/profile/route.ts` around lines 21 - 38, The current findOne → create
flow can race and hit the unique clerkId constraint; replace it with an atomic
upsert or retry-on-duplicate pattern: use Teacher.findOneAndUpdate({ clerkId:
userId }, { $setOnInsert: { clerkId: userId, name: ..., email: ..., department:
'', subjects: [] } }, { upsert: true, new: true, setDefaultsOnInsert: true })
(or wrap Teacher.create in a try/catch that retries a findOne after E11000
duplicate-key error) so that the logic around Teacher.findOne, Teacher.create
and assignment to the teacher variable becomes atomic and race-safe while still
using currentUser() to populate defaults.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant