Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
af03d7a
Validate student create payload
notedwin-dev Apr 18, 2026
f8d2eb9
Require auth for profile fetch
notedwin-dev Apr 18, 2026
edcb3d7
Fix grade creation defaults
notedwin-dev Apr 18, 2026
69c0ea0
Secure grade updates
notedwin-dev Apr 18, 2026
dfdb956
Enforce ownership on record updates
notedwin-dev Apr 18, 2026
1e58563
Validate grade updates
notedwin-dev Apr 18, 2026
7b711cd
Harden grade validation
notedwin-dev Apr 18, 2026
56377f0
Prevent manual grade overrides
notedwin-dev Apr 18, 2026
6b0ca08
Hide internal errors in API responses
notedwin-dev Apr 18, 2026
2ce79ff
Align CGPA grade points
notedwin-dev Apr 18, 2026
bfa2dd9
Handle assignment move failures
notedwin-dev Apr 18, 2026
99cecaf
Fix dashboard assignment counts
notedwin-dev Apr 18, 2026
5f38d94
Fix dashboard student count
notedwin-dev Apr 18, 2026
b15a5c9
Validate empty attendance bulk payload
notedwin-dev Apr 18, 2026
d086b09
Fix assignment edit date input
notedwin-dev Apr 18, 2026
f2a9d80
Handle empty maxMarks in grades form
notedwin-dev Apr 18, 2026
3db6529
Handle empty maxMarks in assignments form
notedwin-dev Apr 18, 2026
918aa22
Default empty grade term
notedwin-dev Apr 18, 2026
f33cc45
Fix auth, validation, and dashboard stats bugs
notedwin-dev Apr 18, 2026
5386ecb
Defer MongoDB env validation
notedwin-dev Apr 18, 2026
60f4cad
Init navbar theme from document
notedwin-dev Apr 18, 2026
82ac82e
Migrate middleware to proxy
notedwin-dev Apr 18, 2026
cfaa5e6
Avoid setState in navbar effect
notedwin-dev Apr 18, 2026
8aa5dab
Align route whitelists with schemas
notedwin-dev Apr 18, 2026
0201d54
Fix A+ threshold at 90 percent
notedwin-dev Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/api/announcements/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import mongoose from 'mongoose'
import { connectDB } from '@/lib/mongodb'
import { Announcement } from '@/models/Announcement'

const ALLOWED_FIELDS = ['title', 'content', 'body', 'audience', 'category', 'pinned', 'expiresAt']
const ALLOWED_FIELDS = ['title', 'content', 'audience', 'category', 'pinned']

export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { userId } = await auth()
Expand Down Expand Up @@ -36,7 +36,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}

const announcement = await Announcement.findOneAndUpdate(
{ _id: id },
{ _id: id, teacherId: userId },
{ $set: sanitizedBody },
{ new: true, runValidators: true, context: 'query' }
)
Expand All @@ -63,7 +63,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str
}

await connectDB()
const deleted = await Announcement.findOneAndDelete({ _id: id })
const deleted = await Announcement.findOneAndDelete({ _id: id, teacherId: userId })

if (!deleted) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
Expand Down
10 changes: 5 additions & 5 deletions app/api/assignments/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import mongoose from 'mongoose'
import { connectDB } from '@/lib/mongodb'
import { Assignment } from '@/models/Assignment'

const ALLOWED_UPDATE_FIELDS = ['title', 'description', 'dueDate', 'deadline', 'subject', 'class', 'status', 'kanbanStatus', 'maxMarks']
const ALLOWED_UPDATE_FIELDS = ['title', 'description', 'deadline', 'subject', 'class', 'status', 'kanbanStatus', 'maxMarks']

export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { userId } = await auth()
Expand Down Expand Up @@ -36,9 +36,9 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}

const assignment = await Assignment.findOneAndUpdate(
{ _id: id },
sanitizedBody,
{ new: true }
{ _id: id, teacherId: userId },
{ $set: sanitizedBody },
{ new: true, runValidators: true, context: 'query' }
)
if (!assignment) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(assignment)
Expand All @@ -63,7 +63,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str
}

await connectDB()
const deleted = await Assignment.findOneAndDelete({ _id: id })
const deleted = await Assignment.findOneAndDelete({ _id: id, teacherId: userId })

if (!deleted) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
Expand Down
2 changes: 1 addition & 1 deletion app/api/assignments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export async function GET(req: NextRequest) {
if (error instanceof Error) {
console.error('GET /api/assignments error:', error.message)
}
return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

Expand Down
6 changes: 6 additions & 0 deletions app/api/attendance/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export async function POST(req: NextRequest) {

// Support both single and bulk
const isBulk = Array.isArray(body)
if (isBulk && body.length === 0) {
return NextResponse.json(
{ error: "Bulk payload must include at least one record" },
{ status: 400 },
);
}
if (isBulk && body.length > 500) {
return NextResponse.json(
{ error: "Bulk payload exceeds maximum of 500 records" },
Expand Down
48 changes: 42 additions & 6 deletions app/api/grades/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ import mongoose from 'mongoose'
import { connectDB } from '@/lib/mongodb'
import { Grade } from '@/models/Grade'

const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks', 'grade']
const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks']

function calcGrade(marks: number, max: number): string {
const pct = (marks / max) * 100
if (pct >= 90) return 'A+'
if (pct >= 80) return 'A'
if (pct >= 70) return 'B+'
if (pct >= 60) return 'B'
if (pct >= 50) return 'C'
if (pct >= 40) return 'D'
return 'F'
}

export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { userId } = await auth()
Expand All @@ -15,7 +26,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string

// Validate ObjectId
if (!mongoose.Types.ObjectId.isValid(id)) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json({ error: 'Invalid id' }, { status: 400 })
}

let body
Expand All @@ -34,10 +45,30 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}

await connectDB()
const existing = await Grade.findOne({ _id: id, teacherId: userId }).lean()
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 })

const updatePayload: Record<string, unknown> = { ...sanitizedBody }
const hasMarks = 'marks' in updatePayload
const hasMaxMarks = 'maxMarks' in updatePayload
if (hasMarks || hasMaxMarks) {
const nextMarks = hasMarks ? Number(updatePayload.marks) : existing.marks
const nextMaxMarks = hasMaxMarks ? Number(updatePayload.maxMarks) : existing.maxMarks
if ((hasMarks && Number.isNaN(nextMarks)) || (hasMaxMarks && Number.isNaN(nextMaxMarks))) {
return NextResponse.json({ error: 'marks and maxMarks must be numbers' }, { status: 400 })
}
if (nextMarks > nextMaxMarks) {
return NextResponse.json({ error: 'marks must be less than or equal to maxMarks' }, { status: 400 })
}
if (hasMarks) updatePayload.marks = nextMarks
if (hasMaxMarks) updatePayload.maxMarks = nextMaxMarks
updatePayload.grade = calcGrade(nextMarks, nextMaxMarks)
}

const grade = await Grade.findOneAndUpdate(
{ _id: id },
sanitizedBody,
{ new: true }
{ _id: id, teacherId: userId },
{ $set: updatePayload },
{ new: true, runValidators: true, context: 'query' }
)
if (!grade) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(grade)
Expand All @@ -55,8 +86,13 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str

try {
const { id } = await ctx.params

if (!mongoose.Types.ObjectId.isValid(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 })
}

await connectDB()
const deleted = await Grade.findOneAndDelete({ _id: id })
const deleted = await Grade.findOneAndDelete({ _id: id, teacherId: userId })

if (!deleted) {
return NextResponse.json({ error: 'Grade not found' }, { status: 404 })
Expand Down
20 changes: 10 additions & 10 deletions app/api/grades/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ const GradeSchema = z.object({
studentName: z.string().min(1),
subject: z.string().min(1),
marks: z.number().min(0),
maxMarks: z.number().min(1).optional(),
maxMarks: z.number().min(1).default(100),
term: z.string().optional(),
}).refine(
(data) => !data.maxMarks || data.marks <= data.maxMarks,
(data) => data.marks <= data.maxMarks,
{
message: 'marks must be less than or equal to maxMarks',
path: ['marks'],
Expand All @@ -21,7 +21,7 @@ const GradeSchema = z.object({

function calcGrade(marks: number, max: number): string {
const pct = (marks / max) * 100
if (pct > 90) return 'A+'
if (pct >= 90) return 'A+'
if (pct >= 80) return 'A'
if (pct >= 70) return 'B+'
if (pct >= 60) return 'B'
Expand All @@ -48,7 +48,7 @@ export async function GET(req: NextRequest) {
return NextResponse.json(grades)
} catch (error) {
console.error('GET /api/grades error:', error instanceof Error ? error.message : error)
return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

Expand All @@ -70,19 +70,19 @@ export async function POST(req: NextRequest) {
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })

const data = parsed.data
const max = data.maxMarks!
const term = data.term ?? 'Term 1'
const maxMarks = data.maxMarks
const term = data.term?.trim() || 'Term 1'

const grade = Grade.findOneAndUpdate(
const grade = await Grade.findOneAndUpdate(
{ teacherId: userId, studentId: data.studentId, subject: data.subject, term },
{ $set: { ...data, term, teacherId: userId, grade: calcGrade(data.marks, max) } },
{ upsert: true, new: true }
{ $set: { ...data, term, maxMarks, teacherId: userId, grade: calcGrade(data.marks, maxMarks) } },
{ upsert: true, new: true, runValidators: true, context: 'query' }
)
return NextResponse.json(grade, { status: 201 })
Comment on lines 72 to 81
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

Empty-term default + upsert can silently overwrite a prior record.

With term defaulted to "Term 1" and the upsert keyed on {teacherId, studentId, subject, term}, two submissions without a term for the same (student, subject) will collide and the second one will overwrite the first under Term 1. If clients commonly omit the term (the client already defaults term to "Term 1" in openAdd, so this is usually fine, but direct API users may not), consider either:

  • Requiring term explicitly at the schema level (z.string().min(1)) and returning 400 if missing, or
  • Returning 200 vs 201 based on whether the upsert created a new document (via rawResult: true / includeResultMetadata: true) so clients can distinguish create from update.

Minor: the response always returns 201 Created even when the upsert merely updates an existing document.

🤖 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 72 - 81, The code currently defaults an
empty term to "Term 1" which lets upserts (Grade.findOneAndUpdate keyed by
teacherId, studentId, subject, term) silently collide; instead make term
required in the request validation (change the Zod schema that produces
parsed.data so term is z.string().min(1)), stop applying a silent default
(remove the `term = data.term?.trim() || 'Term 1'` fallback), and return a 400
Bad Request when parsed.data.term is missing/empty before calling
Grade.findOneAndUpdate; this ensures clients must supply term and prevents
accidental overwrites of existing records.

} catch (error) {
if (error instanceof Error) {
console.error('POST /api/grades error:', error.message)
}
return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
11 changes: 5 additions & 6 deletions app/api/profile/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import { connectDB } from '@/lib/mongodb'
import { Teacher } from '@/models/Teacher'

export async function GET(req: NextRequest) {
const { userId } = await auth()
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

const { searchParams } = new URL(req.url)
const queryUserId = searchParams.get('userId')

let userId: string | null = queryUserId
if (!userId) {
const session = await auth()
userId = session.userId
if (queryUserId && queryUserId !== userId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

try {
await connectDB()
Expand Down
10 changes: 5 additions & 5 deletions app/api/students/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import mongoose from 'mongoose'
import { connectDB } from '@/lib/mongodb'
import { Student } from '@/models/Student'

const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'grade', 'rollNo', 'class', 'phone', 'address', 'parentName', 'parentPhone']
const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'rollNo', 'class', 'phone', 'address', 'parentName', 'parentPhone']

export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { userId } = await auth()
Expand Down Expand Up @@ -35,9 +35,9 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string

await connectDB()
const student = await Student.findOneAndUpdate(
{ _id: id },
sanitizedBody,
{ new: true }
{ _id: id, teacherId: userId },
{ $set: sanitizedBody },
{ new: true, runValidators: true, context: 'query' }
)
if (!student) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(student)
Expand Down Expand Up @@ -65,7 +65,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str
}

await connectDB()
const deleted = await Student.findOneAndDelete({ _id: id })
const deleted = await Student.findOneAndDelete({ _id: id, teacherId: userId })

if (!deleted) {
return NextResponse.json({ error: 'Student not found' }, { status: 404 })
Expand Down
7 changes: 5 additions & 2 deletions app/api/students/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,12 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'Malformed JSON' }, { status: 400 })
}

StudentSchema.safeParse(body)
const parsed = StudentSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })
}

const student = await Student.create({ ...(body as Record<string, unknown>), teacherId: userId })
const student = await Student.create({ ...parsed.data, teacherId: userId })
return NextResponse.json(student, { status: 201 })
} catch (error) {
if (error instanceof Error) {
Expand Down
38 changes: 23 additions & 15 deletions app/dashboard/OverviewClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,27 +189,39 @@ export function OverviewClient() {
const [
studentsRes,
assignmentsRes,
assignmentsActiveRes,
attendanceRes,
gradesRes,
announcementsRes,
] = await Promise.all([
fetch("/api/students?limit=5"),
fetch("/api/assignments"),
fetch("/api/assignments?status=active&limit=1"),
fetch("/api/attendance"),
fetch("/api/grades"),
fetch("/api/announcements?limit=5"),
]);

const [students, assignmentsData, attendance, grades, announcements] =
await Promise.all([
studentsRes.json(),
assignmentsRes.json(),
attendanceRes.json(),
gradesRes.json(),
announcementsRes.json(),
]);
const [
students,
assignmentsData,
assignmentsActiveData,
attendance,
grades,
announcements,
] = await Promise.all([
studentsRes.json(),
assignmentsRes.json(),
assignmentsActiveRes.json(),
attendanceRes.json(),
gradesRes.json(),
announcementsRes.json(),
]);

const assignments = assignmentsData.assignments ?? assignmentsData;
const assignmentsTotal = assignmentsData.total ?? assignments.length;
const pendingAssignments = assignmentsActiveData.total ??
(assignmentsActiveData.assignments ?? assignmentsActiveData ?? []).length;
Comment on lines 198 to +224
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

LGTM on the count fix — but upcomingDeadlines still relies on the paginated list.

assignmentsTotal and pendingAssignments are now correctly derived from server-side total. However, the same assignmentsData.assignments array (line 221) is later used at lines 310–328 to compute upcomingDeadlines, and fetch('/api/assignments') (line 198) uses the server default of limit=20. For teachers whose newest 20 records include closed assignments, active items beyond page 1 will silently disappear from the "Upcoming Deadlines" panel.

Consider either fetching with status=active (and a larger limit) specifically for the deadlines panel, or reusing assignmentsActiveRes with an appropriate limit:

🔧 Suggested adjustment
-        fetch("/api/assignments"),
-        fetch("/api/assignments?status=active&limit=1"),
+        fetch("/api/assignments?limit=100"),
+        fetch("/api/assignments?status=active&limit=100"),

Then derive upcomingDeadlines from assignmentsActiveData.assignments instead of the mixed list.

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

In `@app/dashboard/OverviewClient.tsx` around lines 198 - 224, The
upcomingDeadlines calculation currently uses the paginated assignments list from
assignmentsData (fetched via fetch("/api/assignments") which defaults to
limit=20) and can miss active items beyond page 1; change the data source so
deadlines are computed from the active set: either modify the initial fetch to
request the active set with a sufficiently large limit (e.g.,
fetch("/api/assignments?status=active&limit=100")) or reuse
assignmentsActiveRes/assignmentsActiveData and derive upcomingDeadlines from
assignmentsActiveData.assignments (or perform an additional fetch for active
assignments) so that upcomingDeadlines uses the full active list rather than the
mixed paginated assignments array.


// ── Attendance ──
const dateMap: Record<
Expand Down Expand Up @@ -316,13 +328,9 @@ export function OverviewClient() {
.slice(0, 5);

setStats({
totalStudents: students.students?.length ?? 0,
totalAssignments: Array.isArray(assignments)
? assignments.length
: (assignments.length ?? 0),
pendingAssignments: assignments.filter(
(a: { status: string }) => a.status === "active",
).length,
totalStudents: students.total ?? students.students?.length ?? 0,
totalAssignments: assignmentsTotal,
pendingAssignments,
attendancePct,
attendanceBreakdown: {
present: totalPresent,
Expand Down
15 changes: 12 additions & 3 deletions app/dashboard/assignments/AssignmentsClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export function AssignmentsClient() {
description: a.description,
subject: a.subject,
class: a.class,
deadline: a.deadline,
deadline: a.deadline ? a.deadline.slice(0, 10) : "",
maxMarks: a.maxMarks,
});
setModalOpen(true);
Expand All @@ -356,10 +356,18 @@ export function AssignmentsClient() {
: "/api/assignments";
const method = editing ? "PUT" : "POST";
try {
const normalizedMaxMarks = Number.isFinite(data.maxMarks)
? Number(data.maxMarks)
: undefined;
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...data, maxMarks: Number(data.maxMarks) }),
body: JSON.stringify({
...data,
...(normalizedMaxMarks !== undefined
? { maxMarks: normalizedMaxMarks }
: {}),
}),
});
if (res.ok) {
toast(
Expand Down Expand Up @@ -387,14 +395,15 @@ export function AssignmentsClient() {
),
);
try {
await fetch(`/api/assignments/${id}`, {
const res = await fetch(`/api/assignments/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
kanbanStatus: col,
status: col === "submitted" ? "closed" : "active",
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (error) {
fetchAssignments();
toast(
Expand Down
Loading