Add 8 files, update 14 files

This commit is contained in:
[dyad] committed 2026-08-02 14:00:21 +02:00
1 parent 669e029f45
commit c84b080fb8
22 files changed
+732 -101

No files matched your search

@@ -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 });
}
+33 -11
View File
@@ -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 } },
+62 -5
View File
@@ -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 } },