Files
Vault/app/api/enrollments/sync/route.ts
T
twotalesanimation a834caa511
Deploy Vault CMS / deploy (push) Failing after 38s
Fix watch tracking, video unlocking and access-control gaps
Watch progress and unlocking:
- Player samples the playhead and reports the ranges actually played,
  so skipped sections earn no credit and early plays are not lost
- Server merges ranges into compact segment rows; progress never regresses
- Single 80% unlock rule enforced server-side (lib/watch-progress.ts);
  the Unlock button and automatic unlock now agree
- Lock state and progress bar update live from each progress save
- Playlist is derived from the video, so unlocks work for shared
  playlists and for links from watch history and liked videos
- First/previous video decided by playlist position, not index numbers

Access control:
- Video API and HLS/MP4 routes require a signed-in viewer who is
  enrolled and has the video unlocked; thumbnails require sign-in
- Enrolment sync reads course codes from the allow-list, not the request
- /api/admin/meta is admin-only
- Account status, role and levels are re-checked from the database
  during a session, so deactivated students lose access within a minute

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 16:58:07 +02:00

79 lines
2.6 KiB
TypeScript

// app/api/enrollments/sync/route.ts
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth-options';
import { prisma } from '@/lib/prisma';
import { normalizeEmail } from '@/lib/normalize-email';
/**
* Enrols the signed-in user in the courses listed for them on the allow-list.
* The course codes are read from the database; anything the browser sends is
* ignored, so a user cannot enrol themselves in a course they were not given.
*/
export async function POST() {
try {
const session = await getServerSession(authOptions);
const email = normalizeEmail(session?.user?.email);
if (!email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await prisma.user.findFirst({
where: { email: { equals: email, mode: 'insensitive' } },
select: { id: true },
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
const allowedStudent = await prisma.allowedStudent.findFirst({
where: { email: { equals: email, mode: 'insensitive' } },
select: { levels: true, active: true },
});
const levels = allowedStudent?.active
? allowedStudent.levels
.split(',')
.map((level) => level.trim())
.filter((level) => level.length > 0)
: [];
if (levels.length === 0) {
return NextResponse.json({ message: 'Enrollments synced', created: [] });
}
const courses = await prisma.course.findMany({
where: { code: { in: levels } },
select: { id: true, code: true },
});
const missing = levels.filter((level) => !courses.some((c) => c.code === level));
if (missing.length > 0) {
console.warn(`[SYNC-ENROLLMENTS] Course codes not found: ${missing.join(', ')}`);
}
const created = [];
for (const course of courses) {
const existing = await prisma.enrollment.findUnique({
where: { userId_courseId: { userId: user.id, courseId: course.id } },
select: { id: true },
});
if (existing) continue;
const enrollment = await prisma.enrollment.create({
data: { userId: user.id, courseId: course.id },
});
created.push({
courseCode: course.code,
courseId: course.id,
enrollmentId: enrollment.id,
});
}
return NextResponse.json({ message: 'Enrollments synced', created });
} catch (err) {
console.error('[SYNC-ENROLLMENTS] error:', err);
return NextResponse.json({ error: 'Failed to sync enrollments' }, { status: 500 });
}
}