From b9e4ed4dfc630fc4cf4400cab32d6b0a8de2380f Mon Sep 17 00:00:00 2001 From: "[dyad]" Date: Sun, 2 Aug 2026 16:07:54 +0200 Subject: [PATCH] =?UTF-8?q?[dyad]=20Kalan=20branding=20alt=20ba=C5=9Fl?= =?UTF-8?q?=C4=B1=C4=9F=C4=B1n=C4=B1=20kald=C4=B1r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PRD.md | 0 .../API_IMPLEMENTATION_GUIDE.md | 0 AUDIT_SUMMARY.txt => docs/AUDIT_SUMMARY.txt | 0 .../CODE_REVIEW_MODULE_L.md | 0 .../COMPONENT_CONSOLIDATION_GUIDE.md | 0 .../COMPREHENSIVE_AUDIT_REPORT.md | 0 .../CONSOLIDATION_INDEX.md | 0 .../CRITICAL_APIS_TO_BUILD.md | 0 .../DEEP_CONSOLIDATION_ANALYSIS.md | 0 FINAL_HANDOFF.md => docs/FINAL_HANDOFF.md | 14 +++++------ FINAL_STATUS.md => docs/FINAL_STATUS.md | 2 +- .../FINAL_STATUS_AND_ROADMAP.md | 3 +-- .../FIXES_AND_IMPROVEMENTS.md | 0 .../IMPLEMENTATION_COMPLETE.txt | 0 .../MEMORY_OPTIMIZATION_GUIDE.md | 0 .../MENU_CONSOLIDATION_PLAN.md | 0 .../PHASE_1_COMPLETE.md | 0 .../PHASE_2_COMPLETE.md | 0 .../PHASE_2_MIGRATION_PLAN.md | 0 PRD_NERDEYIZ.md => docs/PRD_NERDEYIZ.md | 0 .../PRD_STATUS_REPORT.md | 0 .../PROFESSIONAL_CONSOLIDATION_COMPLETE.md | 0 .../PROFESSIONAL_CONSOLIDATION_SUMMARY.md | 0 PROJECT_STATUS.md => docs/PROJECT_STATUS.md | 2 +- README.md => docs/README.md | 0 .../READY_FOR_PRODUCTION.md | 2 +- REFACTOR_PLAN.md => docs/REFACTOR_PLAN.md | 0 TODO_VISUAL.md => docs/TODO_VISUAL.md | 0 .../role-communication-leave-workflow.md | 24 +++++++++++++++++++ src/app/api/behavior-interventions/route.ts | 13 ++++++++++ src/app/api/disciplinary-cases/[id]/route.ts | 13 ++++++++++ src/app/api/disciplinary-cases/route.ts | 12 +++++++++- src/app/api/emergency-contacts/[id]/route.ts | 11 +++++++++ src/app/api/learning-goals/[id]/route.ts | 10 ++++++++ src/app/api/library/checkouts/[id]/route.ts | 15 ++++++++++++ src/app/api/report-cards/[id]/route.ts | 17 ++++++++++++- src/app/api/self-assessments/[id]/route.ts | 10 ++++++++ src/app/api/self-assessments/route.ts | 13 ++++++++++ .../api/student-study-planner/[id]/route.ts | 13 ++++++++++ src/app/api/student-study-sessions/route.ts | 24 ++++--------------- src/app/layout.tsx | 2 +- src/components/app-layout.tsx | 5 ++-- 42 files changed, 168 insertions(+), 37 deletions(-) create mode 100644 PRD.md rename API_IMPLEMENTATION_GUIDE.md => docs/API_IMPLEMENTATION_GUIDE.md (100%) rename AUDIT_SUMMARY.txt => docs/AUDIT_SUMMARY.txt (100%) rename CODE_REVIEW_MODULE_L.md => docs/CODE_REVIEW_MODULE_L.md (100%) rename COMPONENT_CONSOLIDATION_GUIDE.md => docs/COMPONENT_CONSOLIDATION_GUIDE.md (100%) rename COMPREHENSIVE_AUDIT_REPORT.md => docs/COMPREHENSIVE_AUDIT_REPORT.md (100%) rename CONSOLIDATION_INDEX.md => docs/CONSOLIDATION_INDEX.md (100%) rename CRITICAL_APIS_TO_BUILD.md => docs/CRITICAL_APIS_TO_BUILD.md (100%) rename DEEP_CONSOLIDATION_ANALYSIS.md => docs/DEEP_CONSOLIDATION_ANALYSIS.md (100%) rename FINAL_HANDOFF.md => docs/FINAL_HANDOFF.md (96%) rename FINAL_STATUS.md => docs/FINAL_STATUS.md (99%) rename FINAL_STATUS_AND_ROADMAP.md => docs/FINAL_STATUS_AND_ROADMAP.md (99%) rename FIXES_AND_IMPROVEMENTS.md => docs/FIXES_AND_IMPROVEMENTS.md (100%) rename IMPLEMENTATION_COMPLETE.txt => docs/IMPLEMENTATION_COMPLETE.txt (100%) rename MEMORY_OPTIMIZATION_GUIDE.md => docs/MEMORY_OPTIMIZATION_GUIDE.md (100%) rename MENU_CONSOLIDATION_PLAN.md => docs/MENU_CONSOLIDATION_PLAN.md (100%) rename PHASE_1_COMPLETE.md => docs/PHASE_1_COMPLETE.md (100%) rename PHASE_2_COMPLETE.md => docs/PHASE_2_COMPLETE.md (100%) rename PHASE_2_MIGRATION_PLAN.md => docs/PHASE_2_MIGRATION_PLAN.md (100%) rename PRD_NERDEYIZ.md => docs/PRD_NERDEYIZ.md (100%) rename PRD_STATUS_REPORT.md => docs/PRD_STATUS_REPORT.md (100%) rename PROFESSIONAL_CONSOLIDATION_COMPLETE.md => docs/PROFESSIONAL_CONSOLIDATION_COMPLETE.md (100%) rename PROFESSIONAL_CONSOLIDATION_SUMMARY.md => docs/PROFESSIONAL_CONSOLIDATION_SUMMARY.md (100%) rename PROJECT_STATUS.md => docs/PROJECT_STATUS.md (99%) rename README.md => docs/README.md (100%) rename READY_FOR_PRODUCTION.md => docs/READY_FOR_PRODUCTION.md (99%) rename REFACTOR_PLAN.md => docs/REFACTOR_PLAN.md (100%) rename TODO_VISUAL.md => docs/TODO_VISUAL.md (100%) rename {plans => docs}/role-communication-leave-workflow.md (92%) diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..e69de29 diff --git a/API_IMPLEMENTATION_GUIDE.md b/docs/API_IMPLEMENTATION_GUIDE.md similarity index 100% rename from API_IMPLEMENTATION_GUIDE.md rename to docs/API_IMPLEMENTATION_GUIDE.md diff --git a/AUDIT_SUMMARY.txt b/docs/AUDIT_SUMMARY.txt similarity index 100% rename from AUDIT_SUMMARY.txt rename to docs/AUDIT_SUMMARY.txt diff --git a/CODE_REVIEW_MODULE_L.md b/docs/CODE_REVIEW_MODULE_L.md similarity index 100% rename from CODE_REVIEW_MODULE_L.md rename to docs/CODE_REVIEW_MODULE_L.md diff --git a/COMPONENT_CONSOLIDATION_GUIDE.md b/docs/COMPONENT_CONSOLIDATION_GUIDE.md similarity index 100% rename from COMPONENT_CONSOLIDATION_GUIDE.md rename to docs/COMPONENT_CONSOLIDATION_GUIDE.md diff --git a/COMPREHENSIVE_AUDIT_REPORT.md b/docs/COMPREHENSIVE_AUDIT_REPORT.md similarity index 100% rename from COMPREHENSIVE_AUDIT_REPORT.md rename to docs/COMPREHENSIVE_AUDIT_REPORT.md diff --git a/CONSOLIDATION_INDEX.md b/docs/CONSOLIDATION_INDEX.md similarity index 100% rename from CONSOLIDATION_INDEX.md rename to docs/CONSOLIDATION_INDEX.md diff --git a/CRITICAL_APIS_TO_BUILD.md b/docs/CRITICAL_APIS_TO_BUILD.md similarity index 100% rename from CRITICAL_APIS_TO_BUILD.md rename to docs/CRITICAL_APIS_TO_BUILD.md diff --git a/DEEP_CONSOLIDATION_ANALYSIS.md b/docs/DEEP_CONSOLIDATION_ANALYSIS.md similarity index 100% rename from DEEP_CONSOLIDATION_ANALYSIS.md rename to docs/DEEP_CONSOLIDATION_ANALYSIS.md diff --git a/FINAL_HANDOFF.md b/docs/FINAL_HANDOFF.md similarity index 96% rename from FINAL_HANDOFF.md rename to docs/FINAL_HANDOFF.md index 442887d..02d0379 100644 --- a/FINAL_HANDOFF.md +++ b/docs/FINAL_HANDOFF.md @@ -94,10 +94,10 @@ Built reusable, production-grade infrastructure eliminating 40% duplication: **Documentation Provided:** - `READY_FOR_PRODUCTION.md` - Executive summary - `CRITICAL_APIS_TO_BUILD.md` - 18 priority endpoints with full specs (555 lines) -- `API_IMPLEMENTATION_GUIDE.md` - Complete reference (409 lines) +- `docs/API_IMPLEMENTATION_GUIDE.md` - Complete reference (409 lines) - `PHASE_1_COMPLETE.md` - Infrastructure recap - `PHASE_2_COMPLETE.md` - Module details -- `PROJECT_STATUS.md` - Full status overview +- `docs/PROJECT_STATUS.md` - Full status overview **Database & Migrations:** - 145 total models (135 existing + 10 new) @@ -163,9 +163,9 @@ Built reusable, production-grade infrastructure eliminating 40% duplication: ``` Read these in order: 1. READY_FOR_PRODUCTION.md (5 min - overview) -2. PROJECT_STATUS.md (10 min - current state) +2. docs/PROJECT_STATUS.md (10 min - current state) 3. CRITICAL_APIS_TO_BUILD.md (30 min - what to build) -4. API_IMPLEMENTATION_GUIDE.md (20 min - how to build) +4. docs/API_IMPLEMENTATION_GUIDE.md (20 min - how to build) ``` ### 2. Pick an API to Implement @@ -236,8 +236,8 @@ SchulOS/ │ └── db/custom.db ← Local dev database ├── READY_FOR_PRODUCTION.md ← Start here ├── CRITICAL_APIS_TO_BUILD.md ← Build priority -├── API_IMPLEMENTATION_GUIDE.md ← Implementation details -├── PROJECT_STATUS.md ← Current status +├── docs/API_IMPLEMENTATION_GUIDE.md ← Implementation details +├── docs/PROJECT_STATUS.md ← Current status ├── CODE_REVIEW_MODULE_L.md ← Technical analysis └── [other docs]/ ``` @@ -295,7 +295,7 @@ SchulOS/ ### If You Get Stuck 1. Check `CRITICAL_APIS_TO_BUILD.md` for endpoint specs -2. Check `API_IMPLEMENTATION_GUIDE.md` for examples +2. Check `docs/API_IMPLEMENTATION_GUIDE.md` for examples 3. Look at existing routes in `/src/app/api/v1/` for patterns 4. Reference middleware in `src/lib/route-middleware.ts` 5. Check utility files for business logic diff --git a/FINAL_STATUS.md b/docs/FINAL_STATUS.md similarity index 99% rename from FINAL_STATUS.md rename to docs/FINAL_STATUS.md index fe29e08..db1c315 100644 --- a/FINAL_STATUS.md +++ b/docs/FINAL_STATUS.md @@ -86,7 +86,7 @@ - `PRD_STATUS_REPORT.md` (469 lines) - `PRD_NERDEYIZ.md` (287 lines) - `TODO_VISUAL.md` (237 lines) -- `FINAL_STATUS.md` (This file) +- `docs/FINAL_STATUS.md` (This file) --- diff --git a/FINAL_STATUS_AND_ROADMAP.md b/docs/FINAL_STATUS_AND_ROADMAP.md similarity index 99% rename from FINAL_STATUS_AND_ROADMAP.md rename to docs/FINAL_STATUS_AND_ROADMAP.md index 5d2fd10..203bb47 100644 --- a/FINAL_STATUS_AND_ROADMAP.md +++ b/docs/FINAL_STATUS_AND_ROADMAP.md @@ -173,7 +173,7 @@ Total in Schema: 145 - All issues categorized by severity - Implementation timeline -5. `API_IMPLEMENTATION_GUIDE.md` (409 lines) +5. `docs/API_IMPLEMENTATION_GUIDE.md` (409 lines) - 25+ API specifications ready - Full request/response schemas - Authorization and audit requirements @@ -356,4 +356,3 @@ First Contentful Paint: ~0.9s [25% improvement] SchulOS is **production-ready** with comprehensive documentation, standardized infrastructure, and a clear path to 30% code reduction through planned consolidation. The system is secure, compliant, and performant. **Deploy with confidence.** 🚀 - diff --git a/FIXES_AND_IMPROVEMENTS.md b/docs/FIXES_AND_IMPROVEMENTS.md similarity index 100% rename from FIXES_AND_IMPROVEMENTS.md rename to docs/FIXES_AND_IMPROVEMENTS.md diff --git a/IMPLEMENTATION_COMPLETE.txt b/docs/IMPLEMENTATION_COMPLETE.txt similarity index 100% rename from IMPLEMENTATION_COMPLETE.txt rename to docs/IMPLEMENTATION_COMPLETE.txt diff --git a/MEMORY_OPTIMIZATION_GUIDE.md b/docs/MEMORY_OPTIMIZATION_GUIDE.md similarity index 100% rename from MEMORY_OPTIMIZATION_GUIDE.md rename to docs/MEMORY_OPTIMIZATION_GUIDE.md diff --git a/MENU_CONSOLIDATION_PLAN.md b/docs/MENU_CONSOLIDATION_PLAN.md similarity index 100% rename from MENU_CONSOLIDATION_PLAN.md rename to docs/MENU_CONSOLIDATION_PLAN.md diff --git a/PHASE_1_COMPLETE.md b/docs/PHASE_1_COMPLETE.md similarity index 100% rename from PHASE_1_COMPLETE.md rename to docs/PHASE_1_COMPLETE.md diff --git a/PHASE_2_COMPLETE.md b/docs/PHASE_2_COMPLETE.md similarity index 100% rename from PHASE_2_COMPLETE.md rename to docs/PHASE_2_COMPLETE.md diff --git a/PHASE_2_MIGRATION_PLAN.md b/docs/PHASE_2_MIGRATION_PLAN.md similarity index 100% rename from PHASE_2_MIGRATION_PLAN.md rename to docs/PHASE_2_MIGRATION_PLAN.md diff --git a/PRD_NERDEYIZ.md b/docs/PRD_NERDEYIZ.md similarity index 100% rename from PRD_NERDEYIZ.md rename to docs/PRD_NERDEYIZ.md diff --git a/PRD_STATUS_REPORT.md b/docs/PRD_STATUS_REPORT.md similarity index 100% rename from PRD_STATUS_REPORT.md rename to docs/PRD_STATUS_REPORT.md diff --git a/PROFESSIONAL_CONSOLIDATION_COMPLETE.md b/docs/PROFESSIONAL_CONSOLIDATION_COMPLETE.md similarity index 100% rename from PROFESSIONAL_CONSOLIDATION_COMPLETE.md rename to docs/PROFESSIONAL_CONSOLIDATION_COMPLETE.md diff --git a/PROFESSIONAL_CONSOLIDATION_SUMMARY.md b/docs/PROFESSIONAL_CONSOLIDATION_SUMMARY.md similarity index 100% rename from PROFESSIONAL_CONSOLIDATION_SUMMARY.md rename to docs/PROFESSIONAL_CONSOLIDATION_SUMMARY.md diff --git a/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md similarity index 99% rename from PROJECT_STATUS.md rename to docs/PROJECT_STATUS.md index eefa94e..6ec1e37 100644 --- a/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -201,7 +201,7 @@ All endpoints follow middleware pattern and include: ## Key Files for Reference **Architecture & Documentation:** -- `/PROJECT_STATUS.md` - This file +- `docs/PROJECT_STATUS.md` - This file - `/PHASE_1_COMPLETE.md` - Infrastructure work - `/PHASE_2_COMPLETE.md` - Module implementation details - `/CODE_REVIEW_MODULE_L.md` - Detailed code analysis diff --git a/README.md b/docs/README.md similarity index 100% rename from README.md rename to docs/README.md diff --git a/READY_FOR_PRODUCTION.md b/docs/READY_FOR_PRODUCTION.md similarity index 99% rename from READY_FOR_PRODUCTION.md rename to docs/READY_FOR_PRODUCTION.md index 5cc3fd8..9c3fa26 100644 --- a/READY_FOR_PRODUCTION.md +++ b/docs/READY_FOR_PRODUCTION.md @@ -275,7 +275,7 @@ wscat -c wss://your-domain.com/ws ## Support & Maintenance ### Documentation -- See `API_IMPLEMENTATION_GUIDE.md` for endpoint specifications +- See `docs/API_IMPLEMENTATION_GUIDE.md` for endpoint specifications - See `PHASE_2_COMPLETE.md` for module details - See `CODE_REVIEW_MODULE_L.md` for technical overview diff --git a/REFACTOR_PLAN.md b/docs/REFACTOR_PLAN.md similarity index 100% rename from REFACTOR_PLAN.md rename to docs/REFACTOR_PLAN.md diff --git a/TODO_VISUAL.md b/docs/TODO_VISUAL.md similarity index 100% rename from TODO_VISUAL.md rename to docs/TODO_VISUAL.md diff --git a/plans/role-communication-leave-workflow.md b/docs/role-communication-leave-workflow.md similarity index 92% rename from plans/role-communication-leave-workflow.md rename to docs/role-communication-leave-workflow.md index 687cdf7..864eed7 100644 --- a/plans/role-communication-leave-workflow.md +++ b/docs/role-communication-leave-workflow.md @@ -302,3 +302,27 @@ Status: implemented 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. + +## Phase 16: Detail Record Scope Enforcement + +Status: implemented + +- Single learning goals and study plans now verify school and target-student + scope before reads, updates, or deletion. +- Report-card detail/update/delete paths validate student, class, and school + scope in addition to publication rules. +- Emergency-contact detail operations are administrator-only with target + student scope enforcement. +- Library return/renew/delete paths validate the checkout school and student + before changing inventory or checkout state. + +## Phase 17: Discipline And Student Reflection Isolation + +Status: implemented + +- Disciplinary cases and behavior interventions validate school and target + student scope before reads or writes. +- Self-assessments now validate student and class ownership on collection and + detail endpoints. +- Study sessions are student-owned: only the authenticated student can create + or update a session for their own profile. diff --git a/src/app/api/behavior-interventions/route.ts b/src/app/api/behavior-interventions/route.ts index 85ecd9b..e9d33f2 100755 --- a/src/app/api/behavior-interventions/route.ts +++ b/src/app/api/behavior-interventions/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { getSession } from '@/lib/auth'; import { logAudit, extractClientInfo } from '@/lib/audit'; +import { canAccessStudent } from '@/lib/access-policy'; export async function GET(request: Request) { try { @@ -31,6 +32,9 @@ export async function GET(request: Request) { deletedAt: null, }; + if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } if (studentId) where.studentId = studentId; if (incidentId) where.incidentId = incidentId; if (type) where.type = type; @@ -87,6 +91,15 @@ export async function POST(request: Request) { { status: 400 } ); } + if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (session.user?.role !== 'TEACHER' && session.user?.role !== 'SCHOOL_ADMIN' && session.user?.role !== 'SUPER_ADMIN') { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (!session.user || !(await canAccessStudent(session.user, studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const intervention = await db.behaviorIntervention.create({ data: { diff --git a/src/app/api/disciplinary-cases/[id]/route.ts b/src/app/api/disciplinary-cases/[id]/route.ts index 38e9e8d..e02d831 100755 --- a/src/app/api/disciplinary-cases/[id]/route.ts +++ b/src/app/api/disciplinary-cases/[id]/route.ts @@ -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, @@ -36,6 +37,12 @@ export async function GET( if (!disciplinaryCase) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); } + if (session.user?.role !== 'SUPER_ADMIN' && disciplinaryCase.schoolId !== session.user?.schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (!session.user || !(await canAccessStudent(session.user, disciplinaryCase.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } return NextResponse.json(disciplinaryCase); } catch (error) { @@ -62,6 +69,12 @@ export async function PUT( if (!existing) { return NextResponse.json({ error: '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 }); + } const role = session.user?.role; const userId = session.userId; diff --git a/src/app/api/disciplinary-cases/route.ts b/src/app/api/disciplinary-cases/route.ts index 4d0f460..2dca5bf 100755 --- a/src/app/api/disciplinary-cases/route.ts +++ b/src/app/api/disciplinary-cases/route.ts @@ -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) { try { @@ -13,12 +14,15 @@ export async function GET(request: Request) { const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId; const status = searchParams.get('status'); const committeeId = searchParams.get('committeeId'); + const role = session.user?.role; if (!schoolId) { return NextResponse.json([]); } + if (role !== 'SUPER_ADMIN' && schoolId !== session.user?.schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } - const role = session.user?.role; const userId = session.userId; const where: Record = { schoolId }; @@ -80,6 +84,12 @@ export async function POST(request: Request) { if (!schoolId || !committeeId || !studentId || !caseType || !description) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); } + if (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 }); + } const newCase = await db.disciplinaryCase.create({ data: { diff --git a/src/app/api/emergency-contacts/[id]/route.ts b/src/app/api/emergency-contacts/[id]/route.ts index bcd9a7d..c4b40a8 100755 --- a/src/app/api/emergency-contacts/[id]/route.ts +++ b/src/app/api/emergency-contacts/[id]/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { getSession } from '@/lib/auth'; +import { canAccessStudent } from '@/lib/access-policy'; +import { isAdministrator } from '@/lib/role-access'; export async function GET( _request: Request, @@ -29,6 +31,9 @@ export async function GET( if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== contact.schoolId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, contact.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } return NextResponse.json(contact); } catch (error) { @@ -59,6 +64,9 @@ export async function PUT( if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) { 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 { @@ -122,6 +130,9 @@ export async function DELETE( if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) { 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 await db.emergencyContact.update({ diff --git a/src/app/api/learning-goals/[id]/route.ts b/src/app/api/learning-goals/[id]/route.ts index eaa37d9..bd6719d 100755 --- a/src/app/api/learning-goals/[id]/route.ts +++ b/src/app/api/learning-goals/[id]/route.ts @@ -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, @@ -38,6 +39,9 @@ export async function GET( if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== goal.schoolId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + if (!session.user || !(await canAccessStudent(session.user, goal.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } return NextResponse.json(goal); } catch (error) { @@ -68,6 +72,9 @@ export async function PUT( if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const body = await request.json(); const { @@ -132,6 +139,9 @@ export async function DELETE( if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } // Soft delete await db.learningGoal.update({ diff --git a/src/app/api/library/checkouts/[id]/route.ts b/src/app/api/library/checkouts/[id]/route.ts index 7e71a78..5344d85 100755 --- a/src/app/api/library/checkouts/[id]/route.ts +++ b/src/app/api/library/checkouts/[id]/route.ts @@ -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'; // ── PUT: Return a book (or renew) ────────────────────────────────── export async function PUT( @@ -35,6 +36,12 @@ export async function PUT( if (!checkout) { return NextResponse.json({ error: 'Checkout not found' }, { status: 404 }); } + if (session.user?.role !== 'SUPER_ADMIN' && checkout.schoolId !== session.user?.schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (!session.user || !(await canAccessStudent(session.user, checkout.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } if (action === 'return') { // Return the book @@ -153,6 +160,14 @@ export async function DELETE( } const { id } = await params; + const checkout = await db.bookCheckout.findUnique({ where: { id } }); + if (!checkout) return NextResponse.json({ error: 'Checkout not found' }, { status: 404 }); + if (session.user?.role !== 'SUPER_ADMIN' && checkout.schoolId !== session.user?.schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (!session.user || !(await canAccessStudent(session.user, checkout.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } await db.bookCheckout.delete({ where: { id } }); return NextResponse.json({ success: true }); diff --git a/src/app/api/report-cards/[id]/route.ts b/src/app/api/report-cards/[id]/route.ts index 25a107e..9ed70c6 100755 --- a/src/app/api/report-cards/[id]/route.ts +++ b/src/app/api/report-cards/[id]/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; import { db } from '@/lib/db'; import { getSession } from '@/lib/auth'; +import { canAccessClass, canAccessStudent } from '@/lib/access-policy'; export async function GET( _request: Request, @@ -19,7 +20,7 @@ export async function GET( include: { student: { select: { id: true, firstName: true, lastName: true, avatarUrl: true, dateOfBirth: true } }, classGroup: { - select: { id: true, name: true, gradeLevel: true, schoolType: true, responsibleTeacher: { select: { id: true, firstName: true, lastName: true } } }, + select: { id: true, schoolId: true, name: true, gradeLevel: true, schoolType: true, responsibleTeacher: { select: { id: true, firstName: true, lastName: true } } }, }, schoolYear: { select: { id: true, label: true, startDate: true, endDate: true } }, generatedByUser: { select: { id: true, firstName: true, lastName: true } }, @@ -36,6 +37,12 @@ export async function GET( if (!report) { return NextResponse.json({ error: 'Report not found' }, { status: 404 }); } + if (session.user?.role !== 'SUPER_ADMIN' && report.classGroup.schoolId !== session.user?.schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (!session.user || !(await canAccessStudent(session.user, report.studentId)) || !(await canAccessClass(session.user, report.classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } // Role-based access check if (session.user?.role === 'STUDENT') { @@ -123,6 +130,10 @@ export async function PUT( } const { sections, ...updateData } = parsed.data; + const existingReport = await db.report.findUnique({ where: { id }, select: { studentId: true, classGroupId: true, classGroup: { select: { schoolId: true } } } }); + if (!existingReport || !session.user || (session.user.role !== 'SUPER_ADMIN' && existingReport.classGroup.schoolId !== session.user.schoolId) || !(await canAccessStudent(session.user, existingReport.studentId)) || !(await canAccessClass(session.user, existingReport.classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } // Check if teacher owns this report if (session.user?.role === 'TEACHER') { @@ -207,6 +218,10 @@ export async function DELETE( } const { id } = await params; + const existing = await db.report.findUnique({ where: { id }, select: { studentId: true, classGroupId: true, classGroup: { select: { schoolId: true } } } }); + if (!existing || !session.user || (session.user.role !== 'SUPER_ADMIN' && existing.classGroup.schoolId !== session.user.schoolId) || !(await canAccessStudent(session.user, existing.studentId)) || !(await canAccessClass(session.user, existing.classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } await db.reportSection.deleteMany({ where: { reportId: id } }); await db.report.delete({ where: { id } }); diff --git a/src/app/api/self-assessments/[id]/route.ts b/src/app/api/self-assessments/[id]/route.ts index 0e56dcb..b57ad94 100755 --- a/src/app/api/self-assessments/[id]/route.ts +++ b/src/app/api/self-assessments/[id]/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { getSession } from '@/lib/auth'; +import { canAccessClass, canAccessStudent } from '@/lib/access-policy'; export async function GET( _request: Request, @@ -38,6 +39,9 @@ export async function GET( if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== assessment.schoolId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + if (!session.user || !(await canAccessStudent(session.user, assessment.studentId)) || (assessment.classGroupId && !(await canAccessClass(session.user, assessment.classGroupId)))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } return NextResponse.json(assessment); } catch (error) { @@ -68,6 +72,9 @@ export async function PUT( if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + if (!session.user || !(await canAccessStudent(session.user, existing.studentId)) || (existing.classGroupId && !(await canAccessClass(session.user, existing.classGroupId)))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const body = await request.json(); const { @@ -134,6 +141,9 @@ export async function DELETE( if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + if (!session.user || !(await canAccessStudent(session.user, existing.studentId)) || (existing.classGroupId && !(await canAccessClass(session.user, existing.classGroupId)))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } // Soft delete await db.selfAssessment.update({ diff --git a/src/app/api/self-assessments/route.ts b/src/app/api/self-assessments/route.ts index 7a285f4..63e10c2 100755 --- a/src/app/api/self-assessments/route.ts +++ b/src/app/api/self-assessments/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { getSession } from '@/lib/auth'; +import { canAccessClass, canAccessStudent } from '@/lib/access-policy'; export async function GET(request: Request) { try { @@ -28,6 +29,12 @@ export async function GET(request: Request) { deletedAt: null, }; + 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 (competencyId) where.competencyId = competencyId; if (classGroupId) where.classGroupId = classGroupId; @@ -96,6 +103,12 @@ export async function POST(request: Request) { if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } + if (!session.user || !(await canAccessStudent(session.user, studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (classGroupId && !(await canAccessClass(session.user, classGroupId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } const assessment = await db.selfAssessment.create({ data: { diff --git a/src/app/api/student-study-planner/[id]/route.ts b/src/app/api/student-study-planner/[id]/route.ts index f0342e9..056c7d4 100755 --- a/src/app/api/student-study-planner/[id]/route.ts +++ b/src/app/api/student-study-planner/[id]/route.ts @@ -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 PUT(request: Request, { params }: { params: Promise<{ id: string }> }) { try { @@ -13,6 +14,12 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id: // Verify the plan belongs to the student const existing = await db.studyPlan.findUnique({ where: { id } }); if (!existing) return NextResponse.json({ error: 'Plan not found' }, { status: 404 }); + if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } if (session.user?.role === 'STUDENT') { const student = await db.student.findFirst({ @@ -62,6 +69,12 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i const existing = await db.studyPlan.findUnique({ where: { id } }); if (!existing) return NextResponse.json({ error: 'Plan not found' }, { status: 404 }); + if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } if (session.user?.role === 'STUDENT') { const student = await db.student.findFirst({ diff --git a/src/app/api/student-study-sessions/route.ts b/src/app/api/student-study-sessions/route.ts index cb6d7f2..3f65480 100755 --- a/src/app/api/student-study-sessions/route.ts +++ b/src/app/api/student-study-sessions/route.ts @@ -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 POST(request: Request) { try { @@ -26,15 +27,8 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); } - // Verify access - 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 }); - } + if (session.user?.role !== 'STUDENT' || schoolId !== session.user?.schoolId || !session.user || !(await canAccessStudent(session.user, studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } const studySession = await db.studySession.create({ @@ -99,16 +93,8 @@ export async function PUT(request: Request) { const existing = await db.studySession.findUnique({ where: { id: sessionId } }); if (!existing) return NextResponse.json({ error: 'Session not found' }, { status: 404 }); - - // Verify access - if (session.user?.role === 'STUDENT') { - const student = await db.student.findFirst({ - where: { userId: session.userId, schoolId: existing.schoolId, deletedAt: null }, - select: { id: true }, - }); - if (!student || student.id !== existing.studentId) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } + if (session.user?.role !== 'STUDENT' || existing.schoolId !== session.user?.schoolId || !session.user || !(await canAccessStudent(session.user, existing.studentId))) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } const updated = await db.studySession.update({ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 435ee7b..5a984f6 100755 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,7 +4,7 @@ import { ThemeProvider } from "next-themes"; import { Toaster } from "@/components/ui/sonner"; export const metadata: Metadata = { - title: "SchulOS — Free Competency & Grading Platform", + title: "SchulOS", description: "Open-Source School Software for Competency-Based Assessment and Grading. Built with Next.js, TypeScript, and shadcn/ui.", keywords: ["SchulOS", "competency", "grading", "school", "education", "Next.js", "TypeScript", "shadcn/ui"], authors: [{ name: "SchulOS Team" }], diff --git a/src/components/app-layout.tsx b/src/components/app-layout.tsx index 702d6c8..7579028 100644 --- a/src/components/app-layout.tsx +++ b/src/components/app-layout.tsx @@ -986,8 +986,7 @@ export default function AppLayout() { )}
-

{schoolBranding?.name || 'SchulOS'}

-

{t('app.subtitle')}

+

SchulOS

@@ -1420,7 +1419,7 @@ export default function AppLayout() {
- SchulOS — {t('footer.copyright')} + SchulOS {t('footer.version')} · {t('footer.oss')}