From 63b81b4764493f8f6034f65b63a73c5046146b37 Mon Sep 17 00:00:00 2001 From: "[dyad]" Date: Sun, 2 Aug 2026 15:18:56 +0200 Subject: [PATCH] Update 3 files --- plans/role-communication-leave-workflow.md | 10 +++++ src/app/api/illness-reports/[id]/route.ts | 11 +++++- .../api/substitutes/absences/[id]/route.ts | 37 ++++++++++++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/plans/role-communication-leave-workflow.md b/plans/role-communication-leave-workflow.md index 2224cd2..fe604e3 100644 --- a/plans/role-communication-leave-workflow.md +++ b/plans/role-communication-leave-workflow.md @@ -207,3 +207,13 @@ Status: implemented vice principals cannot remove conversation records. - Administrative room closure verifies the administrator belongs to the room's school. + +## Phase 8: Leave Record Integrity + +Status: implemented + +- Teacher absence detail endpoints now require authentication, school scope, + and either the absence owner or an administrator. +- Teachers cannot alter status, or edit/delete a leave once it is approved. +- Vice principals cannot delete student leave records; reporters may only + withdraw requests before either approval stage is processed. diff --git a/src/app/api/illness-reports/[id]/route.ts b/src/app/api/illness-reports/[id]/route.ts index c1f7264..0358163 100755 --- a/src/app/api/illness-reports/[id]/route.ts +++ b/src/app/api/illness-reports/[id]/route.ts @@ -189,11 +189,18 @@ export async function DELETE( const role = session.user?.role; const userId = session.userId; - // Only the reporter or admin can delete - if (role !== 'SUPER_ADMIN' && role !== 'SCHOOL_ADMIN' && role !== 'VICE_PRINCIPAL') { + // Only the reporter can withdraw a still-pending request. Final approval + // records cannot be removed by vice principals. + if (role !== 'SUPER_ADMIN' && role !== 'SCHOOL_ADMIN') { if (report.reportedBy !== userId) { return NextResponse.json({ error: 'Only the reporter or admin can delete' }, { status: 403 }); } + if (report.parentApprovalStatus !== 'pending' || report.adminApprovalStatus !== 'pending') { + return NextResponse.json({ error: 'Approved or reviewed reports cannot be deleted by the reporter' }, { status: 400 }); + } + } + if (role !== 'SUPER_ADMIN' && role !== 'STUDENT' && role !== 'PARENT' && report.schoolId !== session.user?.schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } await db.illnessReport.delete({ where: { id } }); diff --git a/src/app/api/substitutes/absences/[id]/route.ts b/src/app/api/substitutes/absences/[id]/route.ts index 377e5a7..80e3c7d 100644 --- a/src/app/api/substitutes/absences/[id]/route.ts +++ b/src/app/api/substitutes/absences/[id]/route.ts @@ -1,9 +1,16 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; +import { getSession } from '@/lib/auth'; + +function canManageAbsence(role: string | undefined, teacherId: string, userId: string): boolean { + return role === 'SUPER_ADMIN' || role === 'SCHOOL_ADMIN' || (role === 'TEACHER' && teacherId === userId); +} // GET /api/substitutes/absences/[id] - Get a single absence export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { + const session = await getSession(); + if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); const { id } = await params; const absence = await db.teacherAbsence.findUnique({ where: { id }, @@ -22,6 +29,12 @@ export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: if (!absence) { return NextResponse.json({ error: 'Absence not found' }, { status: 404 }); } + if (!canManageAbsence(session.user?.role, absence.teacherId, session.userId)) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (session.user?.role !== 'SUPER_ADMIN' && absence.schoolId !== session.user?.schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } return NextResponse.json(absence); } catch (error) { @@ -33,6 +46,8 @@ export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: // PUT /api/substitutes/absences/[id] - Update an absence export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { + const session = await getSession(); + if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); const { id } = await params; const body = await req.json(); @@ -40,9 +55,18 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: if (!existing) { return NextResponse.json({ error: 'Absence not found' }, { status: 404 }); } + if (!canManageAbsence(session.user?.role, existing.teacherId, session.userId)) { + 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 (existing.approvalStatus !== 'pending' && session.user?.role === 'TEACHER') { + return NextResponse.json({ error: 'Approved leave cannot be edited by the teacher' }, { status: 400 }); + } const data: Record = {}; - const simpleFields = ['type', 'reason', 'status', 'notes']; + const simpleFields = session.user?.role === 'TEACHER' ? ['type', 'reason', 'notes'] : ['type', 'reason', 'status', 'notes']; for (const field of simpleFields) { if (body[field] !== undefined) data[field] = body[field]; @@ -70,12 +94,23 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: // DELETE /api/substitutes/absences/[id] - Delete an absence export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { + const session = await getSession(); + if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); const { id } = await params; const existing = await db.teacherAbsence.findUnique({ where: { id } }); if (!existing) { return NextResponse.json({ error: 'Absence not found' }, { status: 404 }); } + if (!canManageAbsence(session.user?.role, existing.teacherId, session.userId)) { + 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 (existing.approvalStatus === 'approved' && session.user?.role === 'TEACHER') { + return NextResponse.json({ error: 'Approved leave cannot be deleted by the teacher' }, { status: 400 }); + } // Delete associated assignments first await db.substitutionAssignment.deleteMany({ where: { absenceId: id } });