a834caa511
Deploy Vault CMS / deploy (push) Failing after 38s
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>
79 lines
2.6 KiB
TypeScript
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 });
|
|
}
|
|
}
|