Update 31 files
This commit is contained in:
1 parent
93ad9aa1e7
commit
71b9ca0eb4
31 files changed
+476
-69
No files matched your search
@@ -217,3 +217,88 @@ Status: implemented
|
|||||||
- Teachers cannot alter status, or edit/delete a leave once it is approved.
|
- Teachers cannot alter status, or edit/delete a leave once it is approved.
|
||||||
- Vice principals cannot delete student leave records; reporters may only
|
- Vice principals cannot delete student leave records; reporters may only
|
||||||
withdraw requests before either approval stage is processed.
|
withdraw requests before either approval stage is processed.
|
||||||
|
|
||||||
|
## Phase 9: Academic And Behavioral Data Isolation
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
- Learning progress reads and writes enforce linked student, class, and teacher
|
||||||
|
scope rather than accepting arbitrary identifiers.
|
||||||
|
- Assessment results validate assessment class access and every target student.
|
||||||
|
- Homework submissions return only the logged-in student or linked parent
|
||||||
|
records, and student submissions resolve from the authenticated user ID.
|
||||||
|
- Behavior incidents are limited to a teacher's assigned classes for reads and
|
||||||
|
writes.
|
||||||
|
|
||||||
|
## Phase 10: Sensitive Student Resource Isolation
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
- Health records are administrator-only and additionally validate the target
|
||||||
|
student scope and school on every operation.
|
||||||
|
- Portfolio entries resolve to the current student or linked parent children
|
||||||
|
when no explicit authorized student is supplied.
|
||||||
|
- Notebook and drawing creation validates class ownership, school ownership,
|
||||||
|
and prevents students from publishing class-visible resources.
|
||||||
|
|
||||||
|
## Phase 11: Personal Planning And Calendar Integrity
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
- Calendar event updates and deletions require event ownership or administrator
|
||||||
|
authority, plus matching school and class access.
|
||||||
|
- Student goals and study plans now use the shared student access predicate for
|
||||||
|
all student, parent, teacher, and administrator callers.
|
||||||
|
- Badge awards and XP changes require an authorized teacher/administrator and
|
||||||
|
an accessible student; character updates cannot cross school boundaries.
|
||||||
|
|
||||||
|
## Phase 12: Wellbeing And Private Record Isolation
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
- Wellness API identity and role now come exclusively from the active session;
|
||||||
|
caller-controlled role query parameters can no longer grant access.
|
||||||
|
- Students may submit only their own daily wellness check-ins; teachers may
|
||||||
|
view only assigned classes.
|
||||||
|
- Teacher notes verify target student scope before reads or writes.
|
||||||
|
- Emergency contacts are restricted to administrators and require school plus
|
||||||
|
authorized target-student scope.
|
||||||
|
|
||||||
|
## Phase 13: Operational Resource Isolation
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
- Student transport records are administrator-only and verify target student
|
||||||
|
scope for read, create, update, and deletion.
|
||||||
|
- Library checkouts validate caller school, target student access, and book
|
||||||
|
school ownership before mutation.
|
||||||
|
- Student resource discovery is limited to public resources targeted at their
|
||||||
|
enrolled classes; resource creation verifies school and class ownership.
|
||||||
|
- School event creation requires a teacher/administrator and class scope;
|
||||||
|
student lists are restricted to all-school or their enrolled-class events.
|
||||||
|
|
||||||
|
## Phase 14: Assessment And Report Isolation
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
- Only students may submit lesson answers or peer assessments, and both flows
|
||||||
|
verify the authenticated student and assigned class.
|
||||||
|
- Teachers can only create/view peer assessment sessions in their own classes;
|
||||||
|
vice principals cannot create them.
|
||||||
|
- Learning goals and report cards enforce the shared student/class access
|
||||||
|
policy for every caller.
|
||||||
|
- Student and parent report lists are restricted to published reports for self
|
||||||
|
or linked children; teacher report lists are limited to assigned classes.
|
||||||
|
|
||||||
|
## Phase 15: Single-Item Resource Integrity
|
||||||
|
|
||||||
|
Status: implemented
|
||||||
|
|
||||||
|
- Homework grading/deletion validates the homework class and target student
|
||||||
|
before a teacher or administrator can mutate a submission.
|
||||||
|
- Student badge queries, awards, and deletions validate school plus target
|
||||||
|
student scope.
|
||||||
|
- Single drawing/resource operations now enforce school and class visibility;
|
||||||
|
student-owned work cannot be republished class-wide.
|
||||||
|
- School-event registration, cancellation, and status changes prohibit user ID
|
||||||
|
spoofing and validate the caller's event school/class access.
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
import { addDays, addWeeks, addMonths, addYears, getDay } from 'date-fns';
|
import { addDays, addWeeks, addMonths, addYears, getDay } from 'date-fns';
|
||||||
|
|
||||||
interface RecurrencePattern {
|
interface RecurrencePattern {
|
||||||
@@ -124,6 +126,15 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
return NextResponse.json({ error: 'Event not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Event not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (!session.user || (!isAdministrator(session.user.role) && existing.teacherId !== session.user.id)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (session.user.role !== 'SUPER_ADMIN' && existing.schoolId !== session.user.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (classGroupId && !(await canAccessClass(session.user, classGroupId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// If editing a single instance of a recurring event
|
// If editing a single instance of a recurring event
|
||||||
if (editMode === 'instance' && existing.parentEventId) {
|
if (editMode === 'instance' && existing.parentEventId) {
|
||||||
@@ -237,6 +248,12 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
return NextResponse.json({ error: 'Event not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Event not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (!session.user || (!isAdministrator(session.user.role) && existing.teacherId !== session.user.id)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (session.user.role !== 'SUPER_ADMIN' && existing.schoolId !== session.user.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// If deleting a single instance, just delete that one
|
// If deleting a single instance, just delete that one
|
||||||
if (deleteMode === 'instance') {
|
if (deleteMode === 'instance') {
|
||||||
|
|||||||
@@ -65,16 +65,23 @@ export async function POST(request: Request) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { requestType, description, addToCalendar, isPrivate } = body;
|
const { requestType, description, addToCalendar, isPrivate } = body;
|
||||||
const schoolId = body.schoolId || session.user?.schoolId;
|
const schoolId = body.schoolId || session.user?.schoolId;
|
||||||
|
const role = session.user?.role;
|
||||||
|
|
||||||
if (!schoolId) {
|
if (!schoolId) {
|
||||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
if (role !== 'SUPER_ADMIN' && schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Only students can request counseling
|
// Only students can request counseling.
|
||||||
const role = session.user?.role;
|
|
||||||
if (role !== 'STUDENT' && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') {
|
if (role !== 'STUDENT' && role !== 'SCHOOL_ADMIN' && role !== 'SUPER_ADMIN') {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (role === 'STUDENT') {
|
||||||
|
const student = await db.student.findFirst({ where: { userId: session.userId, schoolId, deletedAt: null }, select: { id: true } });
|
||||||
|
if (!student) return NextResponse.json({ error: 'Student profile not found' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Find a counselor teacher for this school
|
// Find a counselor teacher for this school
|
||||||
const counselor = await db.user.findFirst({
|
const counselor = await db.user.findFirst({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass } from '@/lib/access-policy';
|
||||||
|
|
||||||
const updateDrawingSchema = z.object({
|
const updateDrawingSchema = z.object({
|
||||||
title: z.string().min(1).optional(),
|
title: z.string().min(1).optional(),
|
||||||
@@ -31,11 +32,17 @@ export async function GET(
|
|||||||
if (!drawing) {
|
if (!drawing) {
|
||||||
return NextResponse.json({ error: 'Drawing not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Drawing not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && drawing.schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Only owner or public drawings
|
// Only owner or public drawings
|
||||||
if (drawing.ownerId !== session.userId && !drawing.isPublic) {
|
if (drawing.ownerId !== session.userId && !drawing.isPublic) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (drawing.isPublic && drawing.classGroupId && (!session.user || !(await canAccessClass(session.user, drawing.classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(drawing);
|
return NextResponse.json(drawing);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -67,6 +74,9 @@ export async function PUT(
|
|||||||
if (existing.ownerId !== session.userId) {
|
if (existing.ownerId !== session.userId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (existing.schoolId !== session.user?.schoolId && session.user?.role !== 'SUPER_ADMIN') {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const parsed = updateDrawingSchema.safeParse(body);
|
const parsed = updateDrawingSchema.safeParse(body);
|
||||||
@@ -76,6 +86,12 @@ export async function PUT(
|
|||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (parsed.data.classGroupId && (!session.user || !(await canAccessClass(session.user, parsed.data.classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (session.user?.role === 'STUDENT' && parsed.data.isPublic) {
|
||||||
|
return NextResponse.json({ error: 'Students cannot publish drawings to a class' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const drawing = await db.drawing.update({
|
const drawing = await db.drawing.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -112,6 +128,9 @@ export async function DELETE(
|
|||||||
if (existing.ownerId !== session.userId && session.user?.role !== 'SUPER_ADMIN' && session.user?.role !== 'SCHOOL_ADMIN') {
|
if (existing.ownerId !== session.userId && session.user?.role !== 'SUPER_ADMIN' && session.user?.role !== 'SCHOOL_ADMIN') {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (existing.schoolId !== session.user?.schoolId && session.user?.role !== 'SUPER_ADMIN') {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
await db.drawing.delete({ where: { id } });
|
await db.drawing.delete({ where: { id } });
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass } from '@/lib/access-policy';
|
||||||
|
|
||||||
const createDrawingSchema = z.object({
|
const createDrawingSchema = z.object({
|
||||||
title: z.string().min(1).default('Untitled Drawing'),
|
title: z.string().min(1).default('Untitled Drawing'),
|
||||||
@@ -71,6 +72,16 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsed.data.schoolId && session.user?.role !== 'SUPER_ADMIN' && parsed.data.schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (parsed.data.classGroupId && (!session.user || !(await canAccessClass(session.user, parsed.data.classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (session.user?.role === 'STUDENT' && parsed.data.isPublic) {
|
||||||
|
return NextResponse.json({ error: 'Students cannot publish drawings to a class' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const drawing = await db.drawing.create({
|
const drawing = await db.drawing.create({
|
||||||
data: {
|
data: {
|
||||||
schoolId: parsed.data.schoolId ?? session.user?.schoolId ?? 'default',
|
schoolId: parsed.data.schoolId ?? session.user?.schoolId ?? 'default',
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -20,12 +22,18 @@ export async function GET(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role)) {
|
||||||
|
return NextResponse.json({ error: 'Emergency contacts are restricted to administrators' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const where: Record<string, unknown> = {
|
const where: Record<string, unknown> = {
|
||||||
schoolId,
|
schoolId,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (studentId) where.studentId = studentId;
|
if (studentId) where.studentId = studentId;
|
||||||
|
|
||||||
const contacts = await db.emergencyContact.findMany({
|
const contacts = await db.emergencyContact.findMany({
|
||||||
@@ -78,6 +86,9 @@ export async function POST(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const contact = await db.emergencyContact.create({
|
const contact = await db.emergencyContact.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_request: Request,
|
_request: Request,
|
||||||
@@ -29,6 +31,9 @@ export async function GET(
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== record.schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== record.schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, record.studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(record);
|
return NextResponse.json(record);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -59,6 +64,9 @@ export async function PUT(
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, existing.studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const {
|
const {
|
||||||
@@ -126,6 +134,9 @@ export async function DELETE(
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, existing.studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Soft delete
|
// Soft delete
|
||||||
await db.healthRecord.update({
|
await db.healthRecord.update({
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -20,12 +22,18 @@ export async function GET(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role)) {
|
||||||
|
return NextResponse.json({ error: 'Health records are restricted to administrators' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const where: Record<string, unknown> = {
|
const where: Record<string, unknown> = {
|
||||||
schoolId,
|
schoolId,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (studentId) where.studentId = studentId;
|
if (studentId) where.studentId = studentId;
|
||||||
|
|
||||||
const records = await db.healthRecord.findMany({
|
const records = await db.healthRecord.findMany({
|
||||||
@@ -80,6 +88,9 @@ export async function POST(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const record = await db.healthRecord.create({
|
const record = await db.healthRecord.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass, canAccessStudent } from '@/lib/access-policy';
|
||||||
|
|
||||||
const gradeSchema = z.object({
|
const gradeSchema = z.object({
|
||||||
score: z.number().min(0).optional().nullable(),
|
score: z.number().min(0).optional().nullable(),
|
||||||
@@ -26,7 +27,7 @@ export async function PUT(
|
|||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { submissionId } = await params;
|
const { id: homeworkId, submissionId } = await params;
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const parsed = gradeSchema.safeParse(body);
|
const parsed = gradeSchema.safeParse(body);
|
||||||
@@ -43,6 +44,10 @@ export async function PUT(
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
return NextResponse.json({ error: 'Submission not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Submission not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
const homework = await db.homework.findUnique({ where: { id: homeworkId }, select: { classGroupId: true } });
|
||||||
|
if (!homework || !session.user || !(await canAccessClass(session.user, homework.classGroupId)) || !(await canAccessStudent(session.user, existing.studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const updateData: Record<string, unknown> = {};
|
const updateData: Record<string, unknown> = {};
|
||||||
if (parsed.data.score !== undefined) {
|
if (parsed.data.score !== undefined) {
|
||||||
@@ -82,13 +87,17 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { submissionId } = await params;
|
const { id: homeworkId, submissionId } = await params;
|
||||||
const existing = await db.homeworkSubmission.findUnique({
|
const existing = await db.homeworkSubmission.findUnique({
|
||||||
where: { id: submissionId, deletedAt: null },
|
where: { id: submissionId, deletedAt: null },
|
||||||
});
|
});
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return NextResponse.json({ error: 'Submission not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Submission not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
const homework = await db.homework.findUnique({ where: { id: homeworkId }, select: { classGroupId: true } });
|
||||||
|
if (!homework || !session.user || !(await canAccessClass(session.user, homework.classGroupId)) || !(await canAccessStudent(session.user, existing.studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
await db.homeworkSubmission.update({
|
await db.homeworkSubmission.update({
|
||||||
where: { id: submissionId },
|
where: { id: submissionId },
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -28,7 +29,18 @@ export async function GET(request: Request) {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (studentId) where.studentId = studentId;
|
if (studentId) where.studentId = studentId;
|
||||||
|
if (!studentId && 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 (!studentId && 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 (competencyId) where.competencyId = competencyId;
|
if (competencyId) where.competencyId = competencyId;
|
||||||
if (status) where.status = status;
|
if (status) where.status = status;
|
||||||
|
|
||||||
@@ -90,6 +102,9 @@ export async function POST(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const goal = await db.learningGoal.create({
|
const goal = await db.learningGoal.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
|
||||||
// ── GET: List checkouts ──────────────────────────────────────────────
|
// ── GET: List checkouts ──────────────────────────────────────────────
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
@@ -22,6 +23,12 @@ export async function GET(request: Request) {
|
|||||||
if (!schoolId) {
|
if (!schoolId) {
|
||||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const where: Record<string, unknown> = { schoolId };
|
const where: Record<string, unknown> = { schoolId };
|
||||||
|
|
||||||
@@ -129,12 +136,21 @@ export async function POST(request: Request) {
|
|||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Check book availability
|
// Check book availability
|
||||||
const book = await db.libraryBook.findUnique({ where: { id: bookId } });
|
const book = await db.libraryBook.findUnique({ where: { id: bookId } });
|
||||||
if (!book) {
|
if (!book) {
|
||||||
return NextResponse.json({ error: 'Book not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Book not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (book.schoolId !== schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Book not found in this school' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
if (book.availableCopies <= 0) {
|
if (book.availableCopies <= 0) {
|
||||||
return NextResponse.json({ error: 'No copies available' }, { status: 400 });
|
return NextResponse.json({ error: 'No copies available' }, { status: 400 });
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass } from '@/lib/access-policy';
|
||||||
|
|
||||||
// ── GET: List notebooks for the current user ──
|
// ── GET: List notebooks for the current user ──
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
@@ -191,6 +192,12 @@ export async function POST(request: Request) {
|
|||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (classGroupId && !(await canAccessClass(session.user, classGroupId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (session.user.role === 'STUDENT' && isPublic) {
|
||||||
|
return NextResponse.json({ error: 'Students cannot publish notebooks to a class' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const ownerType = session.user.role === 'STUDENT' ? 'STUDENT' : 'TEACHER';
|
const ownerType = session.user.role === 'STUDENT' ? 'STUDENT' : 'TEACHER';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass } from '@/lib/access-policy';
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -28,6 +29,9 @@ export async function POST(
|
|||||||
if (!assessmentSession) {
|
if (!assessmentSession) {
|
||||||
return NextResponse.json({ error: 'Session not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Session not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (!session.user || !assessmentSession.classGroupId || !(await canAccessClass(session.user, assessmentSession.classGroupId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
if (assessmentSession.status === 'closed') {
|
if (assessmentSession.status === 'closed') {
|
||||||
return NextResponse.json({ error: 'Session is closed' }, { status: 400 });
|
return NextResponse.json({ error: 'Session is closed' }, { status: 400 });
|
||||||
@@ -55,6 +59,8 @@ export async function POST(
|
|||||||
if (!student || student.id !== assessment.assessorId) {
|
if (!student || student.id !== assessment.assessorId) {
|
||||||
return NextResponse.json({ error: 'You can only submit your own assessments' }, { status: 403 });
|
return NextResponse.json({ error: 'You can only submit your own assessments' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ error: 'Only students can submit peer assessments' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the assessment
|
// Update the assessment
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass, getTeacherClassIds } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -23,6 +25,9 @@ export async function GET(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (classGroupId && (!session.user || !(await canAccessClass(session.user, classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const where: Record<string, unknown> = {
|
const where: Record<string, unknown> = {
|
||||||
schoolId,
|
schoolId,
|
||||||
@@ -42,6 +47,8 @@ export async function GET(request: Request) {
|
|||||||
if (student) {
|
if (student) {
|
||||||
where.assignedPairs = { contains: student.id };
|
where.assignedPairs = { contains: student.id };
|
||||||
}
|
}
|
||||||
|
} else if (session.user?.role === 'TEACHER') {
|
||||||
|
where.classGroupId = { in: await getTeacherClassIds(session.user.id) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessions = await db.peerAssessmentSession.findMany({
|
const sessions = await db.peerAssessmentSession.findMany({
|
||||||
@@ -126,6 +133,14 @@ export async function POST(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (session.user?.role === 'VICE_PRINCIPAL') {
|
||||||
|
return NextResponse.json({ error: 'Vice principals cannot create peer assessments' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (session.user?.role === 'TEACHER') {
|
||||||
|
if ((teacherId && teacherId !== session.user.id) || (classGroupId && !(await canAccessClass(session.user, classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const assessmentSession = await db.peerAssessmentSession.create({
|
const assessmentSession = await db.peerAssessmentSession.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -29,7 +31,22 @@ export async function GET(request: Request) {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (studentId) where.studentId = studentId;
|
if (studentId) where.studentId = studentId;
|
||||||
|
if (!studentId && !isAdministrator(session.user?.role)) {
|
||||||
|
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 (entryType) where.entryType = entryType;
|
if (entryType) where.entryType = entryType;
|
||||||
if (competencyId) where.competencyId = competencyId;
|
if (competencyId) where.competencyId = competencyId;
|
||||||
if (isPublic !== null && isPublic !== undefined) where.isPublic = isPublic === 'true';
|
if (isPublic !== null && isPublic !== undefined) where.isPublic = isPublic === 'true';
|
||||||
@@ -88,6 +105,9 @@ export async function POST(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const entry = await db.portfolioEntry.create({
|
const entry = await db.portfolioEntry.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass, canAccessStudent, getTeacherClassIds } from '@/lib/access-policy';
|
||||||
|
|
||||||
const createReportCardSchema = z.object({
|
const createReportCardSchema = z.object({
|
||||||
studentId: z.string().min(1),
|
studentId: z.string().min(1),
|
||||||
@@ -40,6 +41,12 @@ export async function GET(request: Request) {
|
|||||||
const templateId = searchParams.get('templateId');
|
const templateId = searchParams.get('templateId');
|
||||||
|
|
||||||
const where: Record<string, unknown> = {};
|
const where: Record<string, unknown> = {};
|
||||||
|
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (classGroupId && (!session.user || !(await canAccessClass(session.user, classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (studentId) where.studentId = studentId;
|
if (studentId) where.studentId = studentId;
|
||||||
if (classGroupId) where.classGroupId = classGroupId;
|
if (classGroupId) where.classGroupId = classGroupId;
|
||||||
if (schoolYearId) where.schoolYearId = schoolYearId;
|
if (schoolYearId) where.schoolYearId = schoolYearId;
|
||||||
@@ -70,11 +77,7 @@ export async function GET(request: Request) {
|
|||||||
} else if (session.user?.role === 'TEACHER') {
|
} else if (session.user?.role === 'TEACHER') {
|
||||||
// Teachers see reports for their classes
|
// Teachers see reports for their classes
|
||||||
if (!classGroupId) {
|
if (!classGroupId) {
|
||||||
const teacherClasses = await db.classGroupTeacher.findMany({
|
where.classGroupId = { in: await getTeacherClassIds(session.userId) };
|
||||||
where: { userId: session.userId },
|
|
||||||
});
|
|
||||||
const classIds = teacherClasses.map((c) => c.classGroupId);
|
|
||||||
where.classGroupId = { in: classIds };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +132,9 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { sections, ...reportData } = parsed.data;
|
const { sections, ...reportData } = parsed.data;
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, reportData.studentId)) || !(await canAccessClass(session.user, reportData.classGroupId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// If attendance summary not provided, try to compute it
|
// If attendance summary not provided, try to compute it
|
||||||
let attendanceSummary = reportData.attendanceSummary;
|
let attendanceSummary = reportData.attendanceSummary;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass, canAccessStudent, getTeacherClassIds } from '@/lib/access-policy';
|
||||||
|
|
||||||
const createReportSchema = z.object({
|
const createReportSchema = z.object({
|
||||||
studentId: z.string().min(1),
|
studentId: z.string().min(1),
|
||||||
@@ -34,8 +35,26 @@ export async function GET(request: Request) {
|
|||||||
const status = searchParams.get('status');
|
const status = searchParams.get('status');
|
||||||
|
|
||||||
const where: Record<string, unknown> = {};
|
const where: Record<string, unknown> = {};
|
||||||
|
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (classGroupId && (!session.user || !(await canAccessClass(session.user, classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (studentId) where.studentId = studentId;
|
if (studentId) where.studentId = studentId;
|
||||||
if (classGroupId) where.classGroupId = classGroupId;
|
if (classGroupId) where.classGroupId = classGroupId;
|
||||||
|
if (!studentId && 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;
|
||||||
|
where.status = 'PUBLISHED';
|
||||||
|
} else if (!studentId && 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) };
|
||||||
|
where.status = 'PUBLISHED';
|
||||||
|
} else if (!studentId && session.user?.role === 'TEACHER') {
|
||||||
|
where.classGroupId = { in: await getTeacherClassIds(session.user.id) };
|
||||||
|
}
|
||||||
if (schoolYearId) where.schoolYearId = schoolYearId;
|
if (schoolYearId) where.schoolYearId = schoolYearId;
|
||||||
if (status) where.status = status;
|
if (status) where.status = status;
|
||||||
|
|
||||||
@@ -95,6 +114,9 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { sections, ...reportData } = parsed.data;
|
const { sections, ...reportData } = parsed.data;
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, reportData.studentId)) || !(await canAccessClass(session.user, reportData.classGroupId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const report = await db.report.create({
|
const report = await db.report.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass } from '@/lib/access-policy';
|
||||||
|
|
||||||
const resourceTypeEnum = z.enum([
|
const resourceTypeEnum = z.enum([
|
||||||
'document', 'worksheet', 'presentation', 'video_link', 'image', 'link', 'audio',
|
'document', 'worksheet', 'presentation', 'video_link', 'image', 'link', 'audio',
|
||||||
@@ -48,11 +49,17 @@ export async function GET(
|
|||||||
if (!resource) {
|
if (!resource) {
|
||||||
return NextResponse.json({ error: 'Resource not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Resource not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && resource.schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Students can only see public resources
|
// Students can only see public resources
|
||||||
if (session.user?.role === 'STUDENT' && !resource.isPublic) {
|
if (session.user?.role === 'STUDENT' && !resource.isPublic) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (resource.classGroupId && (!session.user || !(await canAccessClass(session.user, resource.classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(resource);
|
return NextResponse.json(resource);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -88,6 +95,9 @@ export async function PUT(
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
return NextResponse.json({ error: 'Resource not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Resource not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && existing.schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Only the author or admins can edit
|
// Only the author or admins can edit
|
||||||
if (session.user?.role === 'TEACHER' && existing.authorId !== session.userId) {
|
if (session.user?.role === 'TEACHER' && existing.authorId !== session.userId) {
|
||||||
@@ -111,6 +121,9 @@ export async function PUT(
|
|||||||
if (parsed.data.content !== undefined) updateData.content = parsed.data.content;
|
if (parsed.data.content !== undefined) updateData.content = parsed.data.content;
|
||||||
if (parsed.data.subjectId !== undefined) updateData.subjectId = parsed.data.subjectId || null;
|
if (parsed.data.subjectId !== undefined) updateData.subjectId = parsed.data.subjectId || null;
|
||||||
if (parsed.data.classGroupId !== undefined) updateData.classGroupId = parsed.data.classGroupId || null;
|
if (parsed.data.classGroupId !== undefined) updateData.classGroupId = parsed.data.classGroupId || null;
|
||||||
|
if (parsed.data.classGroupId && (!session.user || !(await canAccessClass(session.user, parsed.data.classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (parsed.data.gradeLevel !== undefined) updateData.gradeLevel = parsed.data.gradeLevel;
|
if (parsed.data.gradeLevel !== undefined) updateData.gradeLevel = parsed.data.gradeLevel;
|
||||||
if (parsed.data.tags !== undefined) updateData.tags = parsed.data.tags ? JSON.stringify(parsed.data.tags) : null;
|
if (parsed.data.tags !== undefined) updateData.tags = parsed.data.tags ? JSON.stringify(parsed.data.tags) : null;
|
||||||
if (parsed.data.isPublic !== undefined) updateData.isPublic = parsed.data.isPublic;
|
if (parsed.data.isPublic !== undefined) updateData.isPublic = parsed.data.isPublic;
|
||||||
@@ -150,6 +163,9 @@ export async function DELETE(
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
return NextResponse.json({ error: 'Resource not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Resource not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && existing.schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Only the author or admins can delete
|
// Only the author or admins can delete
|
||||||
if (session.user?.role === 'TEACHER' && existing.authorId !== session.userId) {
|
if (session.user?.role === 'TEACHER' && existing.authorId !== session.userId) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass } from '@/lib/access-policy';
|
||||||
|
|
||||||
const resourceTypeEnum = z.enum([
|
const resourceTypeEnum = z.enum([
|
||||||
'document', 'worksheet', 'presentation', 'video_link', 'image', 'link', 'audio',
|
'document', 'worksheet', 'presentation', 'video_link', 'image', 'link', 'audio',
|
||||||
@@ -50,17 +51,26 @@ export async function GET(request: Request) {
|
|||||||
if (!schoolId) {
|
if (!schoolId) {
|
||||||
return NextResponse.json([]);
|
return NextResponse.json([]);
|
||||||
}
|
}
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const where: Record<string, unknown> = { schoolId, deletedAt: null };
|
const where: Record<string, unknown> = { schoolId, deletedAt: null };
|
||||||
|
|
||||||
// Students can only see public resources
|
// Students can only see public resources
|
||||||
if (session.user?.role === 'STUDENT') {
|
if (session.user?.role === 'STUDENT') {
|
||||||
where.isPublic = true;
|
where.isPublic = true;
|
||||||
|
const student = await db.student.findFirst({ where: { userId: session.user.id, deletedAt: null }, select: { id: true } });
|
||||||
|
const enrollments = student ? await db.enrollment.findMany({ where: { studentId: student.id, endDate: null }, select: { classGroupId: true } }) : [];
|
||||||
|
where.OR = [{ classGroupId: null }, { classGroupId: { in: enrollments.map((enrollment) => enrollment.classGroupId) } }];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceType && resourceType !== 'all') where.resourceType = resourceType;
|
if (resourceType && resourceType !== 'all') where.resourceType = resourceType;
|
||||||
if (subjectId && subjectId !== 'all') where.subjectId = subjectId;
|
if (subjectId && subjectId !== 'all') where.subjectId = subjectId;
|
||||||
if (classGroupId && classGroupId !== 'all') where.classGroupId = classGroupId;
|
if (classGroupId && classGroupId !== 'all') where.classGroupId = classGroupId;
|
||||||
|
if (classGroupId && classGroupId !== 'all' && !(await canAccessClass(session.user!, classGroupId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (isPublic === 'true') where.isPublic = true;
|
if (isPublic === 'true') where.isPublic = true;
|
||||||
if (isPublic === 'false') where.isPublic = false;
|
if (isPublic === 'false') where.isPublic = false;
|
||||||
|
|
||||||
@@ -117,6 +127,9 @@ export async function POST(request: Request) {
|
|||||||
if (session.user?.role === 'SCHOOL_ADMIN' && session.user.schoolId && school.id !== session.user.schoolId) {
|
if (session.user?.role === 'SCHOOL_ADMIN' && session.user.schoolId && school.id !== session.user.schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (session.user?.role === 'TEACHER' && session.user.schoolId !== school.id) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Verify subject if provided
|
// Verify subject if provided
|
||||||
if (subjectId) {
|
if (subjectId) {
|
||||||
@@ -132,6 +145,9 @@ export async function POST(request: Request) {
|
|||||||
if (!classGroup || classGroup.schoolId !== schoolId) {
|
if (!classGroup || classGroup.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Class group not found in this school' }, { status: 404 });
|
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 resource = await db.resource.create({
|
const resource = await db.resource.create({
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -20,6 +22,12 @@ export async function POST(
|
|||||||
if (!event) {
|
if (!event) {
|
||||||
return NextResponse.json({ error: 'Event not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Event not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && event.schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (event.classGroupId && (!session.user || !(await canAccessClass(session.user, event.classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.requiresRegistration) {
|
if (!event.requiresRegistration) {
|
||||||
return NextResponse.json({ error: 'Event does not require registration' }, { status: 400 });
|
return NextResponse.json({ error: 'Event does not require registration' }, { status: 400 });
|
||||||
@@ -36,6 +44,9 @@ export async function POST(
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: 'userId is required' }, { status: 400 });
|
return NextResponse.json({ error: 'userId is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
if (userId !== session.userId && !isAdministrator(session.user?.role)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Check if already registered
|
// Check if already registered
|
||||||
const existing = await db.eventRegistration.findUnique({
|
const existing = await db.eventRegistration.findUnique({
|
||||||
@@ -110,6 +121,13 @@ export async function PUT(
|
|||||||
if (!registration) {
|
if (!registration) {
|
||||||
return NextResponse.json({ error: 'Registration not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Registration not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
const event = await db.schoolEvent.findFirst({ where: { id, deletedAt: null } });
|
||||||
|
if (!event || (session.user?.role !== 'SUPER_ADMIN' && event.schoolId !== session.user?.schoolId)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (userId !== session.userId && !isAdministrator(session.user?.role)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const updated = await db.eventRegistration.update({
|
const updated = await db.eventRegistration.update({
|
||||||
where: { id: registration.id },
|
where: { id: registration.id },
|
||||||
@@ -153,6 +171,13 @@ export async function DELETE(
|
|||||||
if (!registration) {
|
if (!registration) {
|
||||||
return NextResponse.json({ error: 'Registration not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Registration not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
const event = await db.schoolEvent.findFirst({ where: { id, deletedAt: null } });
|
||||||
|
if (!event || (session.user?.role !== 'SUPER_ADMIN' && event.schoolId !== session.user?.schoolId)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (userId !== session.userId && !isAdministrator(session.user?.role)) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel registration (soft delete via status)
|
// Cancel registration (soft delete via status)
|
||||||
const updated = await db.eventRegistration.update({
|
const updated = await db.eventRegistration.update({
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -27,11 +29,18 @@ export async function GET(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (classGroupId && (!session.user || !(await canAccessClass(session.user, classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
const where: Record<string, unknown> = {
|
const where: Record<string, unknown> = {
|
||||||
schoolId,
|
schoolId,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
|
if (session.user?.role === 'STUDENT') {
|
||||||
|
const student = await db.student.findFirst({ where: { userId: session.user.id }, select: { id: true } });
|
||||||
|
const classIds = student ? (await db.enrollment.findMany({ where: { studentId: student.id, endDate: null }, select: { classGroupId: true } })).map((enrollment) => enrollment.classGroupId) : [];
|
||||||
|
where.OR = [{ isAllSchool: true }, { classGroupId: { in: classIds } }];
|
||||||
|
}
|
||||||
|
|
||||||
if (eventType) where.eventType = eventType;
|
if (eventType) where.eventType = eventType;
|
||||||
if (classGroupId) where.classGroupId = classGroupId;
|
if (classGroupId) where.classGroupId = classGroupId;
|
||||||
@@ -131,6 +140,12 @@ export async function POST(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) && session.user?.role !== 'TEACHER') {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (classGroupId && (!session.user || !(await canAccessClass(session.user, classGroupId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const event = await db.schoolEvent.create({
|
const event = await db.schoolEvent.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -114,6 +116,9 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
// Teacher/Admin view
|
// Teacher/Admin view
|
||||||
if (studentIdParam) {
|
if (studentIdParam) {
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentIdParam))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
const studentBadges = await db.studentBadge.findMany({
|
const studentBadges = await db.studentBadge.findMany({
|
||||||
where: { schoolId, studentId: studentIdParam },
|
where: { schoolId, studentId: studentIdParam },
|
||||||
orderBy: { awardedAt: 'desc' },
|
orderBy: { awardedAt: 'desc' },
|
||||||
@@ -164,6 +169,12 @@ export async function POST(request: Request) {
|
|||||||
if (!schoolId || !studentId || !badgeId) {
|
if (!schoolId || !studentId || !badgeId) {
|
||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) && session.user?.role !== 'TEACHER') {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Check if already awarded
|
// Check if already awarded
|
||||||
const existing = await db.studentBadge.findUnique({
|
const existing = await db.studentBadge.findUnique({
|
||||||
@@ -197,6 +208,12 @@ export async function POST(request: Request) {
|
|||||||
if (!schoolId || !studentId || xpAmount === undefined) {
|
if (!schoolId || !studentId || xpAmount === undefined) {
|
||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) && session.user?.role !== 'TEACHER') {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const student = await db.student.findUnique({ where: { id: studentId }, select: { userId: true } });
|
const student = await db.student.findUnique({ where: { id: studentId }, select: { userId: true } });
|
||||||
if (!student?.userId) return NextResponse.json({ error: 'Student has no user account' }, { status: 400 });
|
if (!student?.userId) return NextResponse.json({ error: 'Student has no user account' }, { status: 400 });
|
||||||
@@ -209,6 +226,9 @@ export async function POST(request: Request) {
|
|||||||
if (!characterData) {
|
if (!characterData) {
|
||||||
return NextResponse.json({ error: 'Missing characterData' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing characterData' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
if (schoolId && session.user?.role !== 'SUPER_ADMIN' && schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const character = await db.virtualCharacter.upsert({
|
const character = await db.virtualCharacter.upsert({
|
||||||
where: { userId: session.userId },
|
where: { userId: session.userId },
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
import { withRateLimit } from '@/lib/rate-limit';
|
import { withRateLimit } from '@/lib/rate-limit';
|
||||||
|
import { getTeacherClassIds } from '@/lib/access-policy';
|
||||||
|
|
||||||
// ── GET: List student answers (for the current student) ──
|
// ── GET: List student answers (for the current student) ──
|
||||||
async function getAnswers(request: Request) {
|
async function getAnswers(request: Request) {
|
||||||
@@ -74,6 +75,10 @@ async function getAnswers(request: Request) {
|
|||||||
return NextResponse.json(answers);
|
return NextResponse.json(answers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (session.user.role === 'PARENT') {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Teachers / Admins: can see answers for their school
|
// Teachers / Admins: can see answers for their school
|
||||||
const schoolId = session.user.schoolId;
|
const schoolId = session.user.schoolId;
|
||||||
if (!schoolId) {
|
if (!schoolId) {
|
||||||
@@ -85,6 +90,11 @@ async function getAnswers(request: Request) {
|
|||||||
if (questionId) where.questionId = questionId;
|
if (questionId) where.questionId = questionId;
|
||||||
|
|
||||||
if (lessonId) {
|
if (lessonId) {
|
||||||
|
if (session.user.role === 'TEACHER') {
|
||||||
|
const classIds = await getTeacherClassIds(session.user.id);
|
||||||
|
const lesson = await db.subjectLesson.findFirst({ where: { id: lessonId, topic: { schoolId } }, select: { id: true } });
|
||||||
|
if (!lesson || classIds.length === 0) return NextResponse.json([]);
|
||||||
|
}
|
||||||
const answers = await db.studentAnswer.findMany({
|
const answers = await db.studentAnswer.findMany({
|
||||||
where: {
|
where: {
|
||||||
question: {
|
question: {
|
||||||
@@ -139,6 +149,9 @@ async function submitAnswer(request: Request) {
|
|||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
if (session.user.role !== 'STUDENT') {
|
||||||
|
return NextResponse.json({ error: 'Only students can submit answers' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const parsed = submitAnswerSchema.safeParse(body);
|
const parsed = submitAnswerSchema.safeParse(body);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
|
||||||
function isTeacherOrAdmin(role: string | undefined): boolean {
|
function isTeacherOrAdmin(role: string | undefined): boolean {
|
||||||
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
return role === 'TEACHER' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN';
|
||||||
@@ -26,6 +27,8 @@ export async function GET(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!studentBadge) return NextResponse.json({ error: 'Student badge not found' }, { status: 404 });
|
if (!studentBadge) return NextResponse.json({ error: 'Student badge not found' }, { status: 404 });
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && studentBadge.schoolId !== session.user?.schoolId) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentBadge.studentId))) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
return NextResponse.json(studentBadge);
|
return NextResponse.json(studentBadge);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('StudentBadge GET error:', error);
|
console.error('StudentBadge GET error:', error);
|
||||||
@@ -46,6 +49,8 @@ export async function DELETE(
|
|||||||
|
|
||||||
const existing = await db.studentBadge.findUnique({ where: { id } });
|
const existing = await db.studentBadge.findUnique({ where: { id } });
|
||||||
if (!existing) return NextResponse.json({ error: 'Student badge not found' }, { status: 404 });
|
if (!existing) return NextResponse.json({ error: 'Student badge not found' }, { status: 404 });
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && existing.schoolId !== session.user?.schoolId) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
|
||||||
await db.studentBadge.delete({ where: { id } });
|
await db.studentBadge.delete({ where: { id } });
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
|
||||||
const awardBadgeSchema = z.object({
|
const awardBadgeSchema = z.object({
|
||||||
schoolId: z.string().min(1),
|
schoolId: z.string().min(1),
|
||||||
@@ -35,6 +36,9 @@ export async function GET(request: Request) {
|
|||||||
if (!schoolId) return NextResponse.json([]);
|
if (!schoolId) return NextResponse.json([]);
|
||||||
|
|
||||||
const where: Record<string, unknown> = { schoolId };
|
const where: Record<string, unknown> = { schoolId };
|
||||||
|
if (studentIdParam && (!session.user || !(await canAccessStudent(session.user, studentIdParam)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (studentIdParam) where.studentId = studentIdParam;
|
if (studentIdParam) where.studentId = studentIdParam;
|
||||||
if (badgeIdParam) where.badgeId = badgeIdParam;
|
if (badgeIdParam) where.badgeId = badgeIdParam;
|
||||||
|
|
||||||
@@ -68,14 +72,22 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { schoolId, studentId, badgeId } = parsed.data;
|
const { schoolId, studentId, badgeId } = parsed.data;
|
||||||
|
if (session.user?.role !== 'SUPER_ADMIN' && schoolId !== session.user?.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Verify badge exists
|
// Verify badge exists
|
||||||
const badge = await db.badge.findUnique({ where: { id: badgeId, deletedAt: null } });
|
const badge = await db.badge.findUnique({ where: { id: badgeId, deletedAt: null } });
|
||||||
if (!badge) return NextResponse.json({ error: 'Badge not found' }, { status: 404 });
|
if (!badge) return NextResponse.json({ error: 'Badge not found' }, { status: 404 });
|
||||||
|
if (badge.schoolId !== schoolId) return NextResponse.json({ error: 'Badge not found in this school' }, { status: 404 });
|
||||||
|
|
||||||
// Verify student exists
|
// Verify student exists
|
||||||
const student = await db.student.findUnique({ where: { id: studentId, deletedAt: null } });
|
const student = await db.student.findUnique({ where: { id: studentId, deletedAt: null } });
|
||||||
if (!student) return NextResponse.json({ error: 'Student not found' }, { status: 404 });
|
if (!student) return NextResponse.json({ error: 'Student not found' }, { status: 404 });
|
||||||
|
if (student.schoolId !== schoolId) return NextResponse.json({ error: 'Student not found in this school' }, { status: 404 });
|
||||||
|
|
||||||
// Check if already awarded
|
// Check if already awarded
|
||||||
const existing = await db.studentBadge.findUnique({
|
const existing = await db.studentBadge.findUnique({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -17,24 +18,9 @@ export async function GET(request: Request) {
|
|||||||
return NextResponse.json({ error: 'studentId is required' }, { status: 400 });
|
return NextResponse.json({ error: 'studentId is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the student belongs to the same school or is the student themselves
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
if (session.user?.role === 'STUDENT') {
|
|
||||||
const student = await db.student.findUnique({
|
|
||||||
where: { id: studentId },
|
|
||||||
select: { userId: true, schoolId: true },
|
|
||||||
});
|
|
||||||
if (!student || student.userId !== session.user.id) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
} else if (session.user?.role !== 'SUPER_ADMIN') {
|
|
||||||
const student = await db.student.findUnique({
|
|
||||||
where: { id: studentId },
|
|
||||||
select: { schoolId: true },
|
|
||||||
});
|
|
||||||
if (!student || student.schoolId !== session.user?.schoolId) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const where: Record<string, unknown> = { studentId };
|
const where: Record<string, unknown> = { studentId };
|
||||||
if (status) where.status = status;
|
if (status) where.status = status;
|
||||||
@@ -68,33 +54,9 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the student belongs to the same school or is the student themselves
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
if (session.user?.role === 'STUDENT') {
|
|
||||||
const student = await db.student.findUnique({
|
|
||||||
where: { id: studentId },
|
|
||||||
select: { userId: true, schoolId: true, firstName: true, lastName: true },
|
|
||||||
});
|
|
||||||
if (!student) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
// Check by userId (direct link) or by name matching (fallback for legacy data)
|
|
||||||
const isOwnStudent = student.userId === session.user.id ||
|
|
||||||
(student.userId === null &&
|
|
||||||
student.firstName === session.user.firstName &&
|
|
||||||
student.lastName === session.user.lastName &&
|
|
||||||
student.schoolId === session.user.schoolId);
|
|
||||||
if (!isOwnStudent) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
||||||
}
|
|
||||||
} else if (session.user?.role !== 'SUPER_ADMIN') {
|
|
||||||
const student = await db.student.findUnique({
|
|
||||||
where: { id: studentId },
|
|
||||||
select: { schoolId: true },
|
|
||||||
});
|
|
||||||
if (!student || student.schoolId !== session.user?.schoolId) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const goal = await db.studentGoal.create({
|
const goal = await db.studentGoal.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -34,16 +35,9 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
if (!studentId) return NextResponse.json([]);
|
if (!studentId) return NextResponse.json([]);
|
||||||
|
|
||||||
// Verify access
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
if (session.user?.role === 'STUDENT') {
|
|
||||||
const student = await db.student.findUnique({
|
|
||||||
where: { id: studentId },
|
|
||||||
select: { userId: true },
|
|
||||||
});
|
|
||||||
if (!student || student.userId !== session.userId) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const studyPlans = await db.studyPlan.findMany({
|
const studyPlans = await db.studyPlan.findMany({
|
||||||
where: { schoolId, studentId, status: 'active' },
|
where: { schoolId, studentId, status: 'active' },
|
||||||
@@ -125,16 +119,9 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify access
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
if (session.user?.role === 'STUDENT') {
|
|
||||||
const student = await db.student.findUnique({
|
|
||||||
where: { id: studentId },
|
|
||||||
select: { userId: true, schoolId: true },
|
|
||||||
});
|
|
||||||
if (!student || student.userId !== session.userId || student.schoolId !== schoolId) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const plan = await db.studyPlan.create({
|
const plan = await db.studyPlan.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_request: Request,
|
_request: Request,
|
||||||
@@ -32,6 +34,9 @@ export async function GET(
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== transport.schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== transport.schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, transport.studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(transport);
|
return NextResponse.json(transport);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -62,6 +67,9 @@ export async function PUT(
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, existing.studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const {
|
const {
|
||||||
@@ -130,6 +138,9 @@ export async function DELETE(
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, existing.studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Soft delete
|
// Soft delete
|
||||||
await db.studentTransport.update({
|
await db.studentTransport.update({
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -20,12 +22,18 @@ export async function GET(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role)) {
|
||||||
|
return NextResponse.json({ error: 'Student transport is restricted to administrators' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const where: Record<string, unknown> = {
|
const where: Record<string, unknown> = {
|
||||||
schoolId,
|
schoolId,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
if (studentId) where.studentId = studentId;
|
if (studentId) where.studentId = studentId;
|
||||||
|
|
||||||
const transports = await db.studentTransport.findMany({
|
const transports = await db.studentTransport.findMany({
|
||||||
@@ -82,6 +90,9 @@ export async function POST(request: Request) {
|
|||||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const transport = await db.studentTransport.create({
|
const transport = await db.studentTransport.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getSession } from '@/lib/auth';
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessStudent } from '@/lib/access-policy';
|
||||||
|
|
||||||
const noteCategoryEnum = z.enum([
|
const noteCategoryEnum = z.enum([
|
||||||
'GENERAL',
|
'GENERAL',
|
||||||
@@ -49,6 +50,9 @@ export async function GET(request: Request) {
|
|||||||
if (!student) {
|
if (!student) {
|
||||||
return NextResponse.json({ error: 'Student not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Student not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
session.user?.role === 'SCHOOL_ADMIN' &&
|
session.user?.role === 'SCHOOL_ADMIN' &&
|
||||||
@@ -104,6 +108,9 @@ export async function POST(request: Request) {
|
|||||||
if (!student) {
|
if (!student) {
|
||||||
return NextResponse.json({ error: 'Student not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Student not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
session.user?.role === 'SCHOOL_ADMIN' &&
|
session.user?.role === 'SCHOOL_ADMIN' &&
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { canAccessClass, canAccessStudent } from '@/lib/access-policy';
|
||||||
|
import { isAdministrator } from '@/lib/role-access';
|
||||||
|
|
||||||
// GET /api/wellness — Get wellness data for a student
|
// GET /api/wellness — Get wellness data for a student
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const session = await getSession();
|
||||||
|
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
const schoolId = searchParams.get('schoolId');
|
const schoolId = searchParams.get('schoolId');
|
||||||
const studentId = searchParams.get('studentId');
|
const studentId = searchParams.get('studentId');
|
||||||
const role = searchParams.get('role') || 'STUDENT';
|
const role = session.user.role;
|
||||||
const classGroupId = searchParams.get('classGroupId');
|
const classGroupId = searchParams.get('classGroupId');
|
||||||
const limit = parseInt(searchParams.get('limit') || '30');
|
const limit = parseInt(searchParams.get('limit') || '30');
|
||||||
|
|
||||||
if (!schoolId) {
|
if (!schoolId) {
|
||||||
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
return NextResponse.json({ error: 'schoolId is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
if (role !== 'SUPER_ADMIN' && schoolId !== session.user.schoolId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Admin/VP: school-wide overview
|
// Admin/VP: school-wide overview
|
||||||
if ((role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') && !studentId) {
|
if (isAdministrator(role) && !studentId) {
|
||||||
const totalCheckins = await db.wellnessCheckin.count({
|
const totalCheckins = await db.wellnessCheckin.count({
|
||||||
where: { schoolId },
|
where: { schoolId },
|
||||||
});
|
});
|
||||||
@@ -55,6 +63,9 @@ export async function GET(req: NextRequest) {
|
|||||||
|
|
||||||
// Teacher: class overview
|
// Teacher: class overview
|
||||||
if (role === 'TEACHER' && classGroupId && !studentId) {
|
if (role === 'TEACHER' && classGroupId && !studentId) {
|
||||||
|
if (!(await canAccessClass(session.user, classGroupId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
const enrollments = await db.enrollment.findMany({
|
const enrollments = await db.enrollment.findMany({
|
||||||
where: { classGroupId, endDate: null },
|
where: { classGroupId, endDate: null },
|
||||||
select: { studentId: true },
|
select: { studentId: true },
|
||||||
@@ -82,6 +93,9 @@ export async function GET(req: NextRequest) {
|
|||||||
|
|
||||||
// Student or specific student view
|
// Student or specific student view
|
||||||
if (studentId) {
|
if (studentId) {
|
||||||
|
if (!(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
const checkins = await db.wellnessCheckin.findMany({
|
const checkins = await db.wellnessCheckin.findMany({
|
||||||
where: { schoolId, studentId },
|
where: { schoolId, studentId },
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
@@ -112,12 +126,17 @@ export async function GET(req: NextRequest) {
|
|||||||
// POST /api/wellness — Submit daily check-in
|
// POST /api/wellness — Submit daily check-in
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const session = await getSession();
|
||||||
|
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { schoolId, studentId, mood, sleepHours, sleepQuality, stressLevel, activityType, activityMinutes, mealsCount, waterGlasses, gratitudeEntry, notes } = body;
|
const { schoolId, studentId, mood, sleepHours, sleepQuality, stressLevel, activityType, activityMinutes, mealsCount, waterGlasses, gratitudeEntry, notes } = body;
|
||||||
|
|
||||||
if (!schoolId || !studentId || mood === undefined) {
|
if (!schoolId || !studentId || mood === undefined) {
|
||||||
return NextResponse.json({ error: 'schoolId, studentId, and mood are required' }, { status: 400 });
|
return NextResponse.json({ error: 'schoolId, studentId, and mood are required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
if (session.user.role !== 'STUDENT' || schoolId !== session.user.schoolId || !(await canAccessStudent(session.user, studentId))) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
if (mood < 1 || mood > 5) {
|
if (mood < 1 || mood > 5) {
|
||||||
return NextResponse.json({ error: 'Mood must be between 1 and 5' }, { status: 400 });
|
return NextResponse.json({ error: 'Mood must be between 1 and 5' }, { status: 400 });
|
||||||
|
|||||||
Reference in new issue
Block a user