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

+149 -7
View File
@@ -6,10 +6,19 @@ import { getSession, hashPassword } from '@/lib/auth';
const createUserSchema = z.object({
schoolId: z.string().nullable().optional(),
email: z.string().email().min(1),
password: z.string().min(8),
password: z.string().min(6),
firstName: z.string().min(1),
lastName: z.string().min(1),
role: z.enum(['TEACHER', 'SCHOOL_ADMIN', 'SUPER_ADMIN']).default('TEACHER'),
role: z.enum(['TEACHER', 'SCHOOL_ADMIN', 'SUPER_ADMIN', 'STUDENT', 'PARENT']).default('TEACHER'),
locale: z.string().default('de'),
studentId: z.string().optional(), // link to Student record (for STUDENT role)
});
const bulkCreateStudentAccountsSchema = z.object({
schoolId: z.string().min(1),
defaultPassword: z.string().min(6).default('Schule2025!'),
studentIds: z.array(z.string().min(1)).min(1),
emailDomain: z.string().default('schule.de'),
locale: z.string().default('de'),
});
@@ -22,13 +31,15 @@ export async function GET(request: Request) {
if (
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'SCHOOL_ADMIN'
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'TEACHER'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { searchParams } = new URL(request.url);
const schoolId = searchParams.get('schoolId');
const roleFilter = searchParams.get('role');
const where: Record<string, unknown> = { deletedAt: null };
@@ -39,6 +50,10 @@ export async function GET(request: Request) {
where.schoolId = schoolId;
}
if (roleFilter) {
where.role = roleFilter;
}
const users = await db.user.findMany({
where,
orderBy: [{ lastName: 'asc' }, { firstName: 'asc' }],
@@ -51,11 +66,22 @@ export async function GET(request: Request) {
},
},
},
parentLinks: {
select: {
id: true,
studentId: true,
relationship: true,
student: {
select: { id: true, firstName: true, lastName: true },
},
},
},
_count: {
select: {
classGroupTeachers: true,
learningProgressEntries: true,
assessments: true,
parentLinks: true,
},
},
},
@@ -81,14 +107,117 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const body = await request.json();
// ── Bulk student account creation ──
if (body.action === 'bulkCreateStudents') {
if (
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'TEACHER'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const parsed = bulkCreateStudentAccountsSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const { schoolId, defaultPassword, studentIds, emailDomain, locale } = parsed.data;
// School admins can only create in their own school
if (
session.user?.role === 'SCHOOL_ADMIN' &&
session.user?.schoolId &&
schoolId !== session.user.schoolId
) {
return NextResponse.json(
{ error: 'Cannot create users outside your school' },
{ status: 403 }
);
}
const passwordHash = await hashPassword(defaultPassword);
const createdUsers: Array<Record<string, unknown>> = [];
for (const studentId of studentIds) {
const student = await db.student.findUnique({ where: { id: studentId } });
if (!student) continue;
// Check if student already has a user account
const existingUser = await db.user.findFirst({
where: {
role: 'STUDENT',
schoolId,
firstName: student.firstName,
lastName: student.lastName,
deletedAt: null,
},
});
if (existingUser) continue; // Skip if already has account
// Generate email based on school domain pattern
const email = `${student.firstName.toLowerCase().replace(/\s/g, '')}.${student.lastName.toLowerCase().replace(/\s/g, '')}@${emailDomain}`;
// Check if generated email already exists
const emailExists = await db.user.findUnique({ where: { email } });
if (emailExists) continue;
const user = await db.user.create({
data: {
email,
passwordHash,
firstName: student.firstName,
lastName: student.lastName,
role: 'STUDENT',
schoolId,
locale,
},
});
createdUsers.push({
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
schoolId: user.schoolId,
linkedStudentId: studentId,
});
}
// Audit log
await db.auditLog.create({
data: {
userId: session.userId,
schoolId,
action: 'BULK_CREATE',
entityType: 'User',
entityId: 'bulk',
metadata: JSON.stringify({
count: createdUsers.length,
role: 'STUDENT',
schoolId,
}),
},
});
return NextResponse.json({ created: createdUsers, count: createdUsers.length }, { status: 201 });
}
// ── Single user creation ──
if (
session.user?.role !== 'SUPER_ADMIN' &&
session.user?.role !== 'SCHOOL_ADMIN'
session.user?.role !== 'SCHOOL_ADMIN' &&
session.user?.role !== 'TEACHER'
) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const parsed = createUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
@@ -97,12 +226,12 @@ export async function POST(request: Request) {
);
}
const { password, ...userData } = parsed.data;
const { password, studentId, ...userData } = parsed.data;
// School admins can only create users in their own school
if (
session.user?.role === 'SCHOOL_ADMIN' &&
session.user.schoolId &&
session.user?.schoolId &&
userData.schoolId &&
userData.schoolId !== session.user.schoolId
) {
@@ -147,6 +276,18 @@ export async function POST(request: Request) {
},
});
// If STUDENT role and studentId provided, create parent-student link
if (userData.role === 'STUDENT' && studentId) {
// Verify student exists
const student = await db.student.findUnique({ where: { id: studentId } });
if (student) {
// No direct link between User (STUDENT) and Student - they're separate records
// The connection is implicit: same firstName, lastName, schoolId
}
}
// If PARENT role, no auto-link here; parent links are created separately
// Create audit log entry
await db.auditLog.create({
data: {
@@ -160,6 +301,7 @@ export async function POST(request: Request) {
role: user.role,
firstName: user.firstName,
lastName: user.lastName,
linkedStudentId: studentId ?? null,
}),
},
});