Add 8 files, update 14 files
This commit is contained in:
1 parent
669e029f45
commit
c84b080fb8
22 files changed
+732
-101
No files matched your search
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canAccessClass } from '@/lib/access-policy';
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
@@ -15,6 +16,10 @@ export async function GET(
|
||||
|
||||
const { id: classGroupId } = await params;
|
||||
|
||||
if (!session.user || !(await canAccessClass(session.user, classGroupId))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const enrollments = await db.enrollment.findMany({
|
||||
where: {
|
||||
classGroupId,
|
||||
@@ -56,11 +61,7 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user?.role !== 'SUPER_ADMIN' &&
|
||||
session.user?.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user?.role !== 'TEACHER'
|
||||
) {
|
||||
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.role !== 'SCHOOL_ADMIN') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canAccessRoom } from '@/lib/communication-policy';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
@@ -22,22 +23,15 @@ export async function GET(
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Access control
|
||||
if (role === 'STUDENT' && room.studentId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
} else if (role === 'TEACHER' && room.teacherId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
} else if (role === 'PARENT') {
|
||||
if (role === 'PARENT') {
|
||||
const parentLink = await db.parentStudentLink.findFirst({
|
||||
where: { parentId: userId, studentId: room.studentId },
|
||||
where: { parentId: userId, student: { userId: room.studentId } },
|
||||
});
|
||||
if (!parentLink) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') {
|
||||
if (room.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (!canAccessRoom(role, room, userId, session.user?.schoolId ?? null)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
@@ -95,12 +89,12 @@ export async function POST(
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Only student and teacher can send messages (not admin, not parent)
|
||||
if (role !== 'STUDENT' && role !== 'TEACHER') {
|
||||
return NextResponse.json({ error: 'Only participants can send messages' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (room.studentId !== userId && room.teacherId !== userId) {
|
||||
const membership = await db.communicationRoomMember.findUnique({
|
||||
where: { roomId_userId: { roomId: id, userId } },
|
||||
select: { id: true },
|
||||
});
|
||||
const isSchoolAdministrator = (role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN') && room.escalatedAt;
|
||||
if (!membership && !isSchoolAdministrator) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canAccessRoom, canEscalateToAdmin } from '@/lib/communication-policy';
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
@@ -31,22 +32,16 @@ export async function GET(
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Access control
|
||||
if (role === 'STUDENT' && room.studentId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
} else if (role === 'TEACHER' && room.teacherId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
} else if (role === 'PARENT') {
|
||||
// Direct student rooms remain private until explicitly escalated.
|
||||
if (role === 'PARENT') {
|
||||
const parentLink = await db.parentStudentLink.findFirst({
|
||||
where: { parentId: userId, studentId: room.studentId },
|
||||
where: { parentId: userId, student: { userId: room.studentId } },
|
||||
});
|
||||
if (!parentLink) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL') {
|
||||
if (room.schoolId !== session.user?.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
} else if (!canAccessRoom(role, room, userId, session.user?.schoolId ?? null)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
return NextResponse.json(room);
|
||||
@@ -77,6 +72,31 @@ export async function PUT(
|
||||
const userId = session.userId;
|
||||
const body = await request.json();
|
||||
|
||||
if (body.action === 'escalate') {
|
||||
if (role !== 'STUDENT' && role !== 'PARENT') {
|
||||
return NextResponse.json({ error: 'Only students or parents can escalate' }, { status: 403 });
|
||||
}
|
||||
if (role === 'STUDENT' && room.studentId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (role === 'PARENT') {
|
||||
const student = await db.student.findFirst({ where: { userId: room.studentId }, select: { id: true } });
|
||||
const link = student && await db.parentStudentLink.findFirst({ where: { parentId: userId, studentId: student.id }, select: { id: true } });
|
||||
if (!link) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
if (!canEscalateToAdmin(room)) {
|
||||
return NextResponse.json({ error: 'Escalation becomes available after five business days without resolution' }, { status: 400 });
|
||||
}
|
||||
const admin = await db.user.findFirst({ where: { schoolId: room.schoolId, role: 'SCHOOL_ADMIN', deletedAt: null }, select: { id: true } });
|
||||
if (!admin) return NextResponse.json({ error: 'No school administrator is assigned' }, { status: 400 });
|
||||
await db.communicationRoomMember.upsert({
|
||||
where: { roomId_userId: { roomId: id, userId: admin.id } },
|
||||
update: { role: 'moderator' },
|
||||
create: { roomId: id, userId: admin.id, role: 'moderator' },
|
||||
});
|
||||
return NextResponse.json(await db.communicationRoom.update({ where: { id }, data: { escalatedAt: new Date(), resolutionStatus: 'unresolved' } }));
|
||||
}
|
||||
|
||||
// Teacher accepts a room
|
||||
if (body.status === 'active' && room.status === 'requested') {
|
||||
if (role !== 'TEACHER' || room.teacherId !== userId) {
|
||||
@@ -112,6 +132,8 @@ export async function PUT(
|
||||
status: 'closed',
|
||||
closedAt: new Date(),
|
||||
closeReason: body.closeReason || null,
|
||||
resolvedAt: body.resolved ? new Date() : null,
|
||||
resolutionStatus: body.resolved ? 'resolved' : 'unresolved',
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canCreateDirectRoom, canCreateGroupRoom, escalationEligibleAt } from '@/lib/communication-policy';
|
||||
import { getTeacherClassIds } from '@/lib/access-policy';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
@@ -76,13 +78,65 @@ export async function POST(request: Request) {
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
// Only students can request conversations
|
||||
if (role !== 'STUDENT') {
|
||||
return NextResponse.json({ error: 'Only students can request conversations' }, { status: 403 });
|
||||
const body = await request.json();
|
||||
const { reason, classGroupId, audienceType = 'direct' } = body;
|
||||
|
||||
if (audienceType !== 'direct') {
|
||||
if (!canCreateGroupRoom(role)) {
|
||||
return NextResponse.json({ error: 'Only teachers and administrators can create groups' }, { status: 403 });
|
||||
}
|
||||
if (!classGroupId) {
|
||||
return NextResponse.json({ error: 'classGroupId is required for groups' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (role === 'TEACHER') {
|
||||
const classIds = await getTeacherClassIds(userId);
|
||||
if (!classIds.includes(classGroupId)) {
|
||||
return NextResponse.json({ error: 'Teachers can only create groups for their own classes' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const classGroup = await db.classGroup.findUnique({
|
||||
where: { id: classGroupId },
|
||||
select: { id: true, schoolId: true, responsibleTeacherId: true, enrollments: { where: { endDate: null }, select: { student: { select: { userId: true } } } } },
|
||||
});
|
||||
if (!classGroup || (session.user?.schoolId && classGroup.schoolId !== session.user.schoolId)) {
|
||||
return NextResponse.json({ error: 'Class not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const participantIds = classGroup.enrollments.flatMap((enrollment) => enrollment.student.userId ? [enrollment.student.userId] : []);
|
||||
const moderatorId = classGroup.responsibleTeacherId ?? userId;
|
||||
const room = await db.communicationRoom.create({
|
||||
data: {
|
||||
schoolId: classGroup.schoolId,
|
||||
studentId: userId,
|
||||
teacherId: moderatorId,
|
||||
roomType: 'chat',
|
||||
audienceType,
|
||||
classGroupId,
|
||||
status: 'active',
|
||||
requestedBy: userId,
|
||||
acceptedAt: new Date(),
|
||||
},
|
||||
});
|
||||
await db.communicationRoomMember.createMany({
|
||||
data: [...new Set([userId, moderatorId, ...participantIds])].map((memberId) => ({
|
||||
roomId: room.id,
|
||||
userId: memberId,
|
||||
role: memberId === moderatorId ? 'moderator' : 'member',
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
return NextResponse.json(room, { status: 201 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { reason } = body;
|
||||
if (!canCreateDirectRoom(role)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (role === 'TEACHER') {
|
||||
return NextResponse.json({ error: 'Teachers should use staff communication channels' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Find the student's class and the responsible teacher
|
||||
const student = await db.student.findFirst({
|
||||
@@ -147,6 +201,9 @@ export async function POST(request: Request) {
|
||||
roomType: 'chat',
|
||||
status: 'requested',
|
||||
requestedBy: userId,
|
||||
audienceType: 'direct',
|
||||
classGroupId: currentEnrollment.classGroup.id,
|
||||
escalationEligibleAt: escalationEligibleAt(new Date()),
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
|
||||
@@ -89,8 +89,8 @@ export async function PUT(
|
||||
const userId = session.userId;
|
||||
const body = await request.json();
|
||||
|
||||
// PARENT can approve/reject pending reports
|
||||
if (body.parentApprovalStatus && (role === 'PARENT' || role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL')) {
|
||||
// Parent approval remains parent-only. Final approval is handled by /approve.
|
||||
if (body.parentApprovalStatus && role === 'PARENT') {
|
||||
const newStatus = body.parentApprovalStatus;
|
||||
if (newStatus !== 'approved' && newStatus !== 'rejected') {
|
||||
return NextResponse.json({ error: 'Invalid approval status' }, { status: 400 });
|
||||
@@ -112,8 +112,8 @@ export async function PUT(
|
||||
parentApprovedBy: userId,
|
||||
parentApprovedAt: new Date(),
|
||||
isVisibleToTeacher: newStatus === 'approved',
|
||||
isVisibleToAdmin: newStatus === 'approved',
|
||||
status: newStatus === 'approved' ? 'approved' : 'rejected',
|
||||
isVisibleToAdmin: false,
|
||||
status: newStatus === 'approved' ? 'pending' : 'rejected',
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canAdminApprove, canCreateCalendarEvent, canParentApprove } from '@/lib/leave-policy';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
@@ -12,8 +13,8 @@ export async function POST(request: Request) {
|
||||
const role = session.user?.role;
|
||||
const userId = session.userId;
|
||||
|
||||
if (role !== 'PARENT' && role !== 'SCHOOL_ADMIN' && role !== 'VICE_PRINCIPAL') {
|
||||
return NextResponse.json({ error: 'Only parents or admins can approve' }, { status: 403 });
|
||||
if (!canParentApprove(role) && !canAdminApprove(role)) {
|
||||
return NextResponse.json({ error: 'Only parents or administrators can approve leave requests' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
@@ -32,12 +33,10 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Report not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (report.parentApprovalStatus !== 'pending') {
|
||||
return NextResponse.json({ error: 'Report already processed' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify parent is linked to this student
|
||||
if (role === 'PARENT') {
|
||||
if (report.parentApprovalStatus !== 'pending') {
|
||||
return NextResponse.json({ error: 'Parent approval has already been processed' }, { status: 400 });
|
||||
}
|
||||
const parentLink = await db.parentStudentLink.findFirst({
|
||||
where: { parentId: userId, studentId: report.studentId },
|
||||
});
|
||||
@@ -46,25 +45,78 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
const newStatus = action === 'approve' ? 'approved' : 'rejected';
|
||||
const newStatus = action === 'approve' ? 'approved' : 'rejected';
|
||||
const updated = await db.illnessReport.update({
|
||||
where: { id: reportId },
|
||||
data: {
|
||||
parentApprovalStatus: newStatus,
|
||||
parentApprovedBy: userId,
|
||||
parentApprovedAt: new Date(),
|
||||
isVisibleToTeacher: newStatus === 'approved',
|
||||
status: newStatus === 'approved' ? 'pending' : 'rejected',
|
||||
},
|
||||
include: { student: { include: { enrollments: { where: { endDate: null }, include: { classGroup: { select: { responsibleTeacherId: true } } }, take: 1 } } } },
|
||||
});
|
||||
|
||||
if (newStatus === 'approved') {
|
||||
const teacherId = updated.student.enrollments[0]?.classGroup.responsibleTeacherId;
|
||||
if (teacherId) {
|
||||
await db.notification.create({
|
||||
data: {
|
||||
schoolId: updated.schoolId,
|
||||
userId: teacherId,
|
||||
type: 'LEAVE_PARENT_APPROVED',
|
||||
category: 'administrative',
|
||||
title: 'Student leave request approved by parent',
|
||||
message: `${updated.student.firstName} ${updated.student.lastName} has an approved leave request awaiting administration approval.`,
|
||||
relatedId: updated.id,
|
||||
},
|
||||
});
|
||||
await db.illnessReport.update({ where: { id: updated.id }, data: { teacherNotifiedAt: new Date() } });
|
||||
}
|
||||
}
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
if (report.parentApprovalStatus !== 'approved' || report.adminApprovalStatus !== 'pending') {
|
||||
return NextResponse.json({ error: 'This request is not ready for administrator approval' }, { status: 400 });
|
||||
}
|
||||
if (session.user?.schoolId && report.schoolId !== session.user.schoolId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const adminStatus = action === 'approve' ? 'approved' : 'rejected';
|
||||
const updated = await db.illnessReport.update({
|
||||
where: { id: reportId },
|
||||
data: {
|
||||
parentApprovalStatus: newStatus,
|
||||
parentApprovedBy: userId,
|
||||
parentApprovedAt: new Date(),
|
||||
isVisibleToTeacher: newStatus === 'approved',
|
||||
isVisibleToAdmin: newStatus === 'approved',
|
||||
status: newStatus === 'approved' ? 'approved' : 'rejected',
|
||||
adminApprovalStatus: adminStatus,
|
||||
adminApprovedBy: userId,
|
||||
adminApprovedAt: new Date(),
|
||||
isVisibleToAdmin: true,
|
||||
status: adminStatus,
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
reporter: { select: { id: true, firstName: true, lastName: true } },
|
||||
approver: { select: { id: true, firstName: true, lastName: true } },
|
||||
student: { include: { enrollments: { where: { endDate: null }, include: { classGroup: true }, take: 1 } } },
|
||||
},
|
||||
});
|
||||
|
||||
if (canCreateCalendarEvent(updated)) {
|
||||
const classGroup = updated.student.enrollments[0]?.classGroup;
|
||||
const event = await db.calendarEvent.create({
|
||||
data: {
|
||||
schoolId: updated.schoolId,
|
||||
teacherId: userId,
|
||||
title: `${updated.student.firstName} ${updated.student.lastName}: approved leave`,
|
||||
date: updated.startDate,
|
||||
eventType: 'leave',
|
||||
classGroupId: classGroup?.id ?? null,
|
||||
notes: `Leave type: ${updated.leaveType}`,
|
||||
allDay: true,
|
||||
},
|
||||
});
|
||||
await db.illnessReport.update({ where: { id: updated.id }, data: { calendarEventId: event.id } });
|
||||
}
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
console.error('IllnessReport approve error:', error);
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { studentId, reason, description, startDate, endDate, documentUrl } = body;
|
||||
const { studentId, reason, description, startDate, endDate, documentUrl, leaveType = 'illness' } = body;
|
||||
|
||||
if (!studentId || !reason || !startDate) {
|
||||
return NextResponse.json(
|
||||
@@ -141,8 +141,8 @@ export async function POST(request: Request) {
|
||||
const reporterType = role === 'STUDENT' ? 'student' : 'parent';
|
||||
const isParentReport = reporterType === 'parent';
|
||||
|
||||
// When student reports: needs parent approval, not visible to teacher/admin
|
||||
// When parent reports: auto-approved, visible to teacher/admin
|
||||
// Both student and parent requests require the final administrator approval.
|
||||
// Parent-submitted requests satisfy the parent approval step immediately.
|
||||
const report = await db.illnessReport.create({
|
||||
data: {
|
||||
schoolId: student.schoolId,
|
||||
@@ -154,12 +154,14 @@ export async function POST(request: Request) {
|
||||
startDate: new Date(startDate),
|
||||
endDate: endDate ? new Date(endDate) : null,
|
||||
documentUrl: documentUrl || null,
|
||||
leaveType,
|
||||
parentApprovalStatus: isParentReport ? 'approved' : 'pending',
|
||||
parentApprovedBy: isParentReport ? userId : null,
|
||||
parentApprovedAt: isParentReport ? new Date() : null,
|
||||
isVisibleToTeacher: isParentReport,
|
||||
isVisibleToAdmin: isParentReport,
|
||||
status: isParentReport ? 'approved' : 'pending',
|
||||
isVisibleToAdmin: false,
|
||||
adminApprovalStatus: 'pending',
|
||||
status: 'pending',
|
||||
},
|
||||
include: {
|
||||
student: { select: { id: true, firstName: true, lastName: true } },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canAccessStudent } from '@/lib/access-policy';
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
@@ -14,6 +15,10 @@ export async function GET(
|
||||
|
||||
const { id: studentId } = await params;
|
||||
|
||||
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Load the student with relations
|
||||
const student = await db.student.findUnique({
|
||||
where: { id: studentId, deletedAt: null },
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canAccessStudent, getTeacherClassIds } from '@/lib/access-policy';
|
||||
import { canManageStudent, isAdministrator } from '@/lib/role-access';
|
||||
|
||||
const createStudentSchema = z.object({
|
||||
schoolId: z.string().min(1),
|
||||
@@ -27,10 +29,26 @@ export async function GET(request: Request) {
|
||||
const search = searchParams.get('search');
|
||||
const userId = searchParams.get('userId');
|
||||
|
||||
const user = session.user;
|
||||
if (!user) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
|
||||
const where: Record<string, unknown> = { deletedAt: null };
|
||||
|
||||
if (schoolId) where.schoolId = schoolId;
|
||||
|
||||
if (!isAdministrator(user.role)) {
|
||||
if (user.role === 'TEACHER') {
|
||||
const classIds = await getTeacherClassIds(user.id);
|
||||
where.enrollments = { some: { classGroupId: { in: classIds }, endDate: null } };
|
||||
} else if (user.role === 'STUDENT') {
|
||||
where.userId = user.id;
|
||||
} else if (user.role === 'PARENT') {
|
||||
where.parentStudentLinks = { some: { parentId: user.id } };
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by userId (direct link to User account)
|
||||
let userIdFilter: Record<string, unknown>[] | null = null;
|
||||
if (userId) {
|
||||
@@ -112,11 +130,7 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user?.role !== 'SUPER_ADMIN' &&
|
||||
session.user?.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user?.role !== 'TEACHER'
|
||||
) {
|
||||
if (!canManageStudent(session.user?.role)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
@@ -129,6 +143,13 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
session.user?.role === 'TEACHER' &&
|
||||
(!session.user.schoolId || parsed.data.schoolId !== session.user.schoolId)
|
||||
) {
|
||||
return NextResponse.json({ error: 'Teachers can only create students in their own school' }, { status: 403 });
|
||||
}
|
||||
|
||||
const student = await db.student.create({
|
||||
data: parsed.data,
|
||||
include: {
|
||||
@@ -153,11 +174,7 @@ export async function PUT(request: Request) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (
|
||||
session.user?.role !== 'SUPER_ADMIN' &&
|
||||
session.user?.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user?.role !== 'TEACHER'
|
||||
) {
|
||||
if (!canManageStudent(session.user?.role)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
@@ -185,6 +202,10 @@ export async function PUT(request: Request) {
|
||||
|
||||
const { id, ...updateData } = parsed.data;
|
||||
|
||||
if (!session.user || !(await canAccessStudent(session.user, id))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const student = await db.student.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { canManageUser } from '@/lib/role-access';
|
||||
|
||||
const updateUserSchema = z.object({
|
||||
firstName: z.string().min(1).optional(),
|
||||
@@ -56,6 +57,10 @@ export async function GET(
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!canManageUser(session.user?.role, user.role, user.id === session.userId)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// School admins can only access users from their own school
|
||||
if (
|
||||
session.user?.role === 'SCHOOL_ADMIN' &&
|
||||
@@ -108,6 +113,10 @@ export async function PUT(
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!canManageUser(session.user?.role, existing.role, id === session.userId)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// School admins can only edit users from their own school
|
||||
if (
|
||||
session.user?.role === 'SCHOOL_ADMIN' &&
|
||||
@@ -210,6 +219,10 @@ export async function DELETE(
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!canManageUser(session.user?.role, existing.role, id === session.userId)) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// School admins can only delete users from their own school
|
||||
if (
|
||||
session.user?.role === 'SCHOOL_ADMIN' &&
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession, hashPassword } from '@/lib/auth';
|
||||
import { isAdministrator } from '@/lib/role-access';
|
||||
|
||||
const createUserSchema = z.object({
|
||||
schoolId: z.string().nullable().optional(),
|
||||
@@ -31,8 +32,7 @@ export async function GET(request: Request) {
|
||||
|
||||
if (
|
||||
session.user?.role !== 'SUPER_ADMIN' &&
|
||||
session.user?.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user?.role !== 'TEACHER'
|
||||
session.user?.role !== 'SCHOOL_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
@@ -113,8 +113,7 @@ export async function POST(request: Request) {
|
||||
if (body.action === 'bulkCreateStudents') {
|
||||
if (
|
||||
session.user?.role !== 'SUPER_ADMIN' &&
|
||||
session.user?.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user?.role !== 'TEACHER'
|
||||
session.user?.role !== 'SCHOOL_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
@@ -212,8 +211,7 @@ export async function POST(request: Request) {
|
||||
// ── Single user creation ──
|
||||
if (
|
||||
session.user?.role !== 'SUPER_ADMIN' &&
|
||||
session.user?.role !== 'SCHOOL_ADMIN' &&
|
||||
session.user?.role !== 'TEACHER'
|
||||
session.user?.role !== 'SCHOOL_ADMIN'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
@@ -228,7 +226,7 @@ export async function POST(request: Request) {
|
||||
|
||||
const { password, studentId, ...userData } = parsed.data;
|
||||
|
||||
// School admins can only create users in their own school
|
||||
// Administrators can only create users in their own school.
|
||||
if (
|
||||
session.user?.role === 'SCHOOL_ADMIN' &&
|
||||
session.user?.schoolId &&
|
||||
@@ -242,10 +240,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// School admins cannot create super admin accounts
|
||||
if (
|
||||
session.user?.role === 'SCHOOL_ADMIN' &&
|
||||
userData.role === 'SUPER_ADMIN'
|
||||
) {
|
||||
if (session.user?.role === 'SCHOOL_ADMIN' && userData.role === 'SUPER_ADMIN') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot create super admin accounts' },
|
||||
{ status: 403 }
|
||||
|
||||
Reference in new issue
Block a user