Round 11: DrawingCanvas Integration, Notebook Versioning, Student Auth, Parent Portal, Calendar Recurring, Grade PDF, Data Import/Export

This commit is contained in:
Mustafa Cetin committed 2026-07-29 09:15:59 +00:00
1 parent b90ad85b91
commit c3d36aac67
22 files changed
+3984 -314

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
export async function GET(
request: Request,
{ 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 link = await db.parentStudentLink.findUnique({
where: { id },
include: {
parent: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
},
},
student: {
select: {
id: true,
firstName: true,
lastName: true,
dateOfBirth: true,
externalId: true,
schoolId: true,
enrollments: {
select: {
classGroup: {
select: { id: true, name: true, gradeLevel: true },
},
},
},
},
},
school: {
select: { id: true, name: true },
},
},
});
if (!link) {
return NextResponse.json({ error: 'Link not found' }, { status: 404 });
}
// Parents can only view their own links
if (session.user?.role === 'PARENT' && link.parentId !== session.user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.json(link);
} catch (error) {
console.error('ParentLink GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function PUT(
request: Request,
{ 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 request.json();
const { relationship } = body;
const link = await db.parentStudentLink.findUnique({ where: { id } });
if (!link) {
return NextResponse.json({ error: 'Link not found' }, { status: 404 });
}
// Only admins, teachers, or the parent can update
if (
session.user?.role !== 'TEACHER' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'SUPER_ADMIN' &&
(session.user?.role === 'PARENT' && link.parentId !== session.user.id)
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const updated = await db.parentStudentLink.update({
where: { id },
data: { relationship: relationship ?? null },
include: {
parent: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
},
},
student: {
select: {
id: true,
firstName: true,
lastName: true,
schoolId: true,
},
},
school: {
select: { id: true, name: true },
},
},
});
return NextResponse.json(updated);
} catch (error) {
console.error('ParentLink PUT error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function DELETE(
request: Request,
{ 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 link = await db.parentStudentLink.findUnique({ where: { id } });
if (!link) {
return NextResponse.json({ error: 'Link not found' }, { status: 404 });
}
// Only admins, teachers, or the parent can delete
if (
session.user?.role !== 'TEACHER' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'SUPER_ADMIN' &&
(session.user?.role === 'PARENT' && link.parentId !== session.user.id)
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
await db.parentStudentLink.delete({ where: { id } });
return NextResponse.json({ message: 'Link removed' });
} catch (error) {
console.error('ParentLink DELETE error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+218
View File
@@ -0,0 +1,218 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
const createLinkSchema = z.object({
parentId: z.string().min(1),
studentId: z.string().min(1),
schoolId: z.string().min(1),
relationship: z.enum(['mother', 'father', 'guardian', 'other']).optional(),
});
export async function GET(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const parentId = searchParams.get('parentId');
// If parentId is provided, get links for that parent
// If user is PARENT role, they can only see their own links
let filter: Record<string, unknown> = {};
if (session.user?.role === 'PARENT') {
filter.parentId = session.user.id;
} else if (parentId) {
// Teachers/admins can query by parentId
filter.parentId = parentId;
} else if (session.user?.role === 'TEACHER' || session.user?.role === 'SCHOOL_ADMIN') {
// Teachers/admins can see all links in their school
if (session.user?.schoolId) {
filter.schoolId = session.user.schoolId;
}
}
const links = await db.parentStudentLink.findMany({
where: filter,
include: {
parent: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
},
},
student: {
select: {
id: true,
firstName: true,
lastName: true,
dateOfBirth: true,
externalId: true,
schoolId: true,
enrollments: {
select: {
classGroup: {
select: { id: true, name: true, gradeLevel: true },
},
},
},
},
},
school: {
select: { id: true, name: true },
},
},
orderBy: [{ createdAt: 'desc' }],
});
return NextResponse.json(links);
} catch (error) {
console.error('ParentLinks GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
// Only teachers, school admins, or the parent themselves can create links
if (
session.user?.role !== 'TEACHER' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'PARENT'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const parsed = createLinkSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const { parentId, studentId, schoolId, relationship } = parsed.data;
// Verify parent user has PARENT role
const parentUser = await db.user.findUnique({ where: { id: parentId } });
if (!parentUser || parentUser.role !== 'PARENT') {
return NextResponse.json({ error: 'User must have PARENT role' }, { status: 400 });
}
// Verify student exists
const student = await db.student.findUnique({ where: { id: studentId } });
if (!student) {
return NextResponse.json({ error: 'Student not found' }, { status: 404 });
}
// Check if link already exists
const existing = await db.parentStudentLink.findUnique({
where: { parentId_studentId: { parentId, studentId } },
});
if (existing) {
return NextResponse.json({ error: 'Link already exists' }, { status: 409 });
}
// Parents can only create links for themselves
if (session.user?.role === 'PARENT' && parentId !== session.user.id) {
return NextResponse.json({ error: 'Parents can only link their own account' }, { status: 403 });
}
// School admins can only create links in their school
if (
session.user?.role === 'SCHOOL_ADMIN' &&
session.user?.schoolId &&
schoolId !== session.user.schoolId
) {
return NextResponse.json({ error: 'Cannot create links outside your school' }, { status: 403 });
}
const link = await db.parentStudentLink.create({
data: {
parentId,
studentId,
schoolId,
relationship: relationship ?? null,
},
include: {
parent: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
},
},
student: {
select: {
id: true,
firstName: true,
lastName: true,
schoolId: true,
},
},
school: {
select: { id: true, name: true },
},
},
});
return NextResponse.json(link, { status: 201 });
} catch (error) {
console.error('ParentLinks POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function DELETE(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const linkId = searchParams.get('id');
if (!linkId) {
return NextResponse.json({ error: 'Link ID required' }, { status: 400 });
}
const link = await db.parentStudentLink.findUnique({ where: { id: linkId } });
if (!link) {
return NextResponse.json({ error: 'Link not found' }, { status: 404 });
}
// Only admins, teachers, or the parent themselves can delete
if (
session.user?.role !== 'TEACHER' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'SUPER_ADMIN' &&
(session.user?.role === 'PARENT' && link.parentId !== session.user.id)
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
await db.parentStudentLink.delete({ where: { id: linkId } });
return NextResponse.json({ message: 'Link removed' });
} catch (error) {
console.error('ParentLinks DELETE error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}