From 93ad9aa1e790c48263e7d0ac5b7dd1e82ce538f8 Mon Sep 17 00:00:00 2001 From: "[dyad]" Date: Sun, 2 Aug 2026 15:22:26 +0200 Subject: [PATCH] Update 4 files --- src/app/api/assessments/[id]/results/route.ts | 15 ++++++++ src/app/api/behavior-incidents/route.ts | 16 +++++++++ .../api/homework/[id]/submissions/route.ts | 17 +++++++++ src/app/api/learning-progress/route.ts | 35 +++++++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/src/app/api/assessments/[id]/results/route.ts b/src/app/api/assessments/[id]/results/route.ts index 696b3ee..0fc2a1e 100755 --- a/src/app/api/assessments/[id]/results/route.ts +++ b/src/app/api/assessments/[id]/results/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; import { db } from '@/lib/db'; import { getSession } from '@/lib/auth'; +import { canAccessClass, canAccessStudent } from '@/lib/access-policy'; const resultSchema = z.object({ studentId: z.string().min(1), @@ -23,6 +24,10 @@ export async function GET( } const { id: assessmentId } = await params; + const assessment = await db.assessment.findUnique({ where: { id: assessmentId }, select: { classGroupId: true } }); + if (!assessment || !session.user || !(await canAccessClass(session.user, assessment.classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const results = await db.assessmentResult.findMany({ where: { assessmentId }, @@ -66,6 +71,10 @@ export async function POST( } const { id: assessmentId } = await params; + const assessment = await db.assessment.findUnique({ where: { id: assessmentId }, select: { classGroupId: true } }); + if (!assessment || !session.user || !(await canAccessClass(session.user, assessment.classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const body = await request.json(); const isBulk = Array.isArray(body); @@ -82,6 +91,9 @@ export async function POST( // Upsert each result (assessmentId + studentId is unique) const results: Array<{ id: string; studentId: string; score: number | null; masteryLevelValue: number | null; note: string | null; assessmentId: string }> = []; for (const item of parsed.data) { + if (!(await canAccessStudent(session.user!, item.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const result = await db.assessmentResult.upsert({ where: { assessmentId_studentId: { @@ -117,6 +129,9 @@ export async function POST( { status: 400 } ); } + if (!(await canAccessStudent(session.user!, parsed.data.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const result = await db.assessmentResult.upsert({ where: { diff --git a/src/app/api/behavior-incidents/route.ts b/src/app/api/behavior-incidents/route.ts index 6727522..763c5e4 100755 --- a/src/app/api/behavior-incidents/route.ts +++ b/src/app/api/behavior-incidents/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; import { db } from '@/lib/db'; import { getSession } from '@/lib/auth'; +import { canAccessClass, canAccessStudent, getTeacherClassIds } from '@/lib/access-policy'; const severityEnum = z.enum(['minor', 'moderate', 'major']); @@ -60,6 +61,15 @@ export async function GET(request: Request) { } const where: Record = { schoolId }; + if (session.user?.role === 'TEACHER') { + where.classGroupId = { in: await getTeacherClassIds(session.user.id) }; + } + if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (classGroupId && classGroupId !== 'all' && (!session.user || !(await canAccessClass(session.user, classGroupId)))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } if (studentId) where.studentId = studentId; if (classGroupId && classGroupId !== 'all') where.classGroupId = classGroupId; if (categoryId && categoryId !== 'all') where.categoryId = categoryId; @@ -153,6 +163,9 @@ export async function POST(request: Request) { if (student.deletedAt) { return NextResponse.json({ error: 'Student is deleted' }, { status: 400 }); } + if (!session.user || !(await canAccessStudent(session.user, studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } // Verify category belongs to same school const category = await db.behaviorCategory.findUnique({ @@ -172,6 +185,9 @@ export async function POST(request: Request) { if (!classGroup || classGroup.schoolId !== schoolId) { return NextResponse.json({ error: 'Class group not found in this school' }, { status: 404 }); } + if (!session.user || !(await canAccessClass(session.user, classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } } const resolved = parsed.data.resolved ?? false; diff --git a/src/app/api/homework/[id]/submissions/route.ts b/src/app/api/homework/[id]/submissions/route.ts index afea7af..1b1863c 100755 --- a/src/app/api/homework/[id]/submissions/route.ts +++ b/src/app/api/homework/[id]/submissions/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; import { db } from '@/lib/db'; import { getSession } from '@/lib/auth'; +import { canAccessClass, canAccessStudent } from '@/lib/access-policy'; const submitSchema = z.object({ content: z.string().max(10000).optional().nullable(), @@ -27,7 +28,19 @@ export async function GET( const { searchParams } = new URL(request.url); const status = searchParams.get('status'); + const homework = await db.homework.findUnique({ where: { id, deletedAt: null }, select: { classGroupId: true } }); + if (!homework || !session.user || !(await canAccessClass(session.user, homework.classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const where: Record = { homeworkId: id, deletedAt: null }; + if (session.user.role === 'STUDENT') { + const student = await db.student.findFirst({ where: { userId: session.user.id }, select: { id: true } }); + if (!student) return NextResponse.json([]); + where.studentId = student.id; + } else if (session.user.role === 'PARENT') { + const links = await db.parentStudentLink.findMany({ where: { parentId: session.user.id }, select: { studentId: true } }); + where.studentId = { in: links.map((link) => link.studentId) }; + } if (status && status !== 'all') where.status = status; const submissions = await db.homeworkSubmission.findMany({ @@ -81,6 +94,7 @@ export async function POST( // Student users need to find their student record via enrollment const student = await db.student.findFirst({ where: { + userId: session.user.id, schoolId: homework.schoolId, deletedAt: null, enrollments: { @@ -98,6 +112,9 @@ export async function POST( if (!studentId) { return NextResponse.json({ error: 'studentId is required' }, { status: 400 }); } + if (!session.user || !(await canAccessStudent(session.user, studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } } if (!studentId) { diff --git a/src/app/api/learning-progress/route.ts b/src/app/api/learning-progress/route.ts index e56d1f8..9ad508b 100755 --- a/src/app/api/learning-progress/route.ts +++ b/src/app/api/learning-progress/route.ts @@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; import { db } from '@/lib/db'; import { getSession } from '@/lib/auth'; +import { canAccessClass, canAccessStudent, getTeacherClassIds } from '@/lib/access-policy'; +import { isAdministrator } from '@/lib/role-access'; const entrySchema = z.object({ studentId: z.string().min(1), @@ -30,8 +32,29 @@ export async function GET(request: Request) { const endDate = searchParams.get('endDate'); const where: Record = {}; + if (!session.user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + if (studentId && !(await canAccessStudent(session.user, studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (classGroupId && !(await canAccessClass(session.user, classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } if (studentId) where.studentId = studentId; if (classGroupId) where.classGroupId = classGroupId; + if (!isAdministrator(session.user.role) && !studentId && !classGroupId) { + if (session.user.role === 'TEACHER') { + where.classGroupId = { in: await getTeacherClassIds(session.user.id) }; + } else if (session.user.role === 'STUDENT') { + const student = await db.student.findFirst({ where: { userId: session.user.id }, select: { id: true } }); + if (!student) return NextResponse.json([]); + where.studentId = student.id; + } else if (session.user.role === 'PARENT') { + const links = await db.parentStudentLink.findMany({ where: { parentId: session.user.id }, select: { studentId: true } }); + where.studentId = { in: links.map((link) => link.studentId) }; + } else { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + } if (competencyId) where.competencyId = competencyId; if (startDate || endDate) { @@ -78,6 +101,10 @@ export async function POST(request: Request) { const body = await request.json(); + if (session.user?.role !== 'TEACHER' && !isAdministrator(session.user?.role)) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + // Support both single entry and bulk (array) entries const isBulk = Array.isArray(body); @@ -90,6 +117,11 @@ export async function POST(request: Request) { ); } + for (const entry of parsed.data) { + if (entry.teacherId !== session.userId || !(await canAccessStudent(session.user!, entry.studentId)) || !(await canAccessClass(session.user!, entry.classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + } const entries = await db.learningProgressEntry.createMany({ data: parsed.data, }); @@ -106,6 +138,9 @@ export async function POST(request: Request) { { status: 400 } ); } + if (parsed.data.teacherId !== session.userId || !(await canAccessStudent(session.user!, parsed.data.studentId)) || !(await canAccessClass(session.user!, parsed.data.classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const entry = await db.learningProgressEntry.create({ data: parsed.data,